Impact is currently in release candidate and aims to solve some key pain points moving to global state management. With React 19 and new async patterns this library at the very least has some relevant perspectives.
The talk was on-point. I fully agree with the points and also feel the need for a better
state management solution.
The library has an interesting approach that might work, but I have some reservations:
1. subscribing with the using keyword feels scary as I don't have a conceptual model of what's going on. Why isn't useEffect enough here?
2. As I understand, stores can be initialised with props, but how would I go about passing dynamic values into the store? For instance, what if I want to pass a react-router search param into the StoreProvider whenever it changes?
3. an issue that I have with all state management solutions is that, when you wan't to integrate with another state-manager like react-query, you need to fall back to hooks as the lowest-common denominator. Maybe it would help if Signals supported Observables in addition to promises, so that we can build a
4. related to 2 and 3, I often hold my state in the url or in local storage. A universal state management solution would need to integrate with these external sources. Otherwise, much of the derived state will be computed in the components rather than the stores.
Hi Sebastian and thanks so much for your feedback :)
1. So with observation an `ObserverContext` is created when using a store in a component. This "ObserverContext" tracks what signals you access in the component. This "ObserverContext" has to end its tracking when the component function exits. This is what `using` does. It runs logic when the component function exits to stop tracking signals. Then indeed we run a `useEffect`, or rather `syncExternalStore`, subscribing to the `ObserverContext` created. Traditionally you would have to wrap your component function in a higher order component, but then you have an additional import and you have to define your components as variable assignments. This also affects debugging as your components are typically not named with this signature. So yeah, think of `using` as "tracking signal access in the component scope" :)
2. So until now I have not been in a scenario where you would need to do this. Either you use a "key" on the `StoreProvider` to unmount and mount with new props. Which makes sense if the prop for example represents a user or a ticket in a project management app. Typically subscriptions created internally in the store would be its dynamic nature. That said, this does bring up a very interesting idea. We could make the props signals. So you would access the props in a store like any other signal you define in the store. I need to reflect a bit more on it and create a POC, but it is something we should implement before the official release as it would be a breaking change later. Thanks a bunch for this perspective!
3. A bit of text was lost there, but as I understand you suggest giving first class support for traditional Observables in signals? And you mean RxJS types of observables? I did reflect on this early on. Like Angular has a `toSignal` on their observables. This is something that can easily be added later given that we find a good way to execute on it. I'll experiment some more on this, thanks for bringing it up :)
4. Impact is considered a much lower level API than traditional global state management. So if you want to synchronise a signal with a url or local storage you would currently have to do that manually. But as a lower level API I hope to see people build abstractions. For example there could be a local storage sync API that looks like:
```
function MyStore() {
const count = signal(0)
const flag = signal(false)
syncLocalStorage('$SOME-KEY', { count, flag })
return {
get count() {
return count()
},
get flag() {
return flag()
}
}
}
```
In this case the `syncLocalStorage` would be a two way sync with local storage. You define the initial signals and then any changes to them will be synced to local storage. Any changes in local storage is synced back to them, including any initial value. When the store unmounts, the sync is disposed of.
But yeah, I am not sure to what degree these things should be added to the library (It adds maintenance cost which I have been hurt by before). I want to first see if the community sees these opportunities and creates abstractions :)
Again, thanks a bunch for the feedback, I hope my responses made sense and have a great day!
1. that's actually pretty neat. Collecting dependencies like Signals but without the callback... I retract my reservations :)
3. I was thinking of any arbitrary event source that supports the typical subscribe/unsubscribe pattern. Either push based like rxjs or pull based like Reacts useSyncExternalStore. rxjs would probably require some extra work to integrate error handling, and I prefer that the pull based approach is just generally more friendly to debugging, tracing & stack traces.
4. That example looks reasonable to me, although we're now once again running into the problem of starting with uninitialised state and only later populating it. In the case of local storage it's not much of a problem since you need a default-value anyways, but what if we try to derive data from react-query? I agree that the framework itself should be agnostic of the actual storage method though.
2, 3 and 4 are all different facets of the same requirement: An observability solution must be able to derive from any external source, or we are back to doing most of our derivations inside react hooks. At least that's the issue I always ran into when dealing with redux, react-query & co. I would have 40% of my state in redux, 40% in react-query, 10% in the URL, 10% in local storage. Then I would do most of the heavy transformations with react hooks, because that's the only place which allows me to combine all sources. Im aware that some of this could be solved with redux middleware / rtk-query, but then you're once again loosing type-safety due to every component needing to handle the uninitialised case.
You were absolutely right in saying that observability is better than reconciliation for state management. However, I think this does not only apply to state management but to all types of derived data, including server state, local storage etc...
Yes, what you are saying makes a lot of sense. And traditionally you would need to do some combination of a global state store like Redux, react-query and custom hooks.
Impact does support a new type of asynchronous pattern with React, which I am very excited about. It is supported in React 18 using the polyfilled `use` hook in Impact, but with React 19 you can use the `use` hook of React itself. What this means is that you do not have to differentiate sync/async values in signals. Signals makes promises observable. So:
const data = signal(fetchData())
const count = signal(0)
Both are observable values, where the data promise can be consumed in components with the `use` hook or by checking its data.status property directly.
So instead of having to split your state management between redux, react-query and hooks you are now able to just think state and co locate it with the same primitives and abstractions using stores.
If you read the https://impact-react.dev/deep-dive/queries-and-mutations docs, you'll see how the low level usage of this is. I would expect someone to create a similar data store abstraction for Impact like react-query, to manage expiration etc. at a higher level, like:
const DataStore = createDataStore(...)
which allows you to:
function MyComponent({ id }) {
using dataStore = useDataStore()
const data = use(dataStore.fetch(id))
}
But unlike react-query it would be based on observable promises.
But yeah, I do not want to state that Impact should just "do everything", but it is a low enough abstraction to co locate all state management in the same abstraction, if that is a priority. It might still feel too low level at the moment, but I am very curious to see where it goes.
When it comes to RxJS types of observables it is possible to do the same as `toSignal` in Angular, but I am unsure if it should be a first class concept like promises. Maybe it should, but also something the community can build :)
1. Fetch data about the content (promise)
2. Use that to instantiate a process (promise)
3. But async instantiation of this process also emitted events during instantiation we wanted to display as part of loading the process
4. Finally mount state management for this data bound to the process
Previously these steps where multiple components, but with this new async pattern we where able to express it all in a single component, line by line, using a store. It blew my mind :)
I get what you're saying in that document, but I would still prefer react-query. Although your example seems simple enough, it would become increasingly complex once you think about swr, caching, persistence, invalidation-timers etc...
Requiring everything to be based on your low-level implementation seems like a tall ask. One thing that I love about React is how easily it can be configured to read from any reactivity-solution. This makes it possible to integrate pre-existing code or libraries. In impact, I see two possible clean entry points for external state: the Provider props, or a signal constructor similar to useSyncExternal store.
I did some experimentation to see if I can write a function that converts an rxjs stream to a signal, but it involved multiple dirty hacks such as extracting the resolve/reject functions from the promise callback.
I just published a version with the signal props. Doing this made me realise that it creates this strong concept that StoreProviders becomes the bridge between the reconciling world of React and the observable world of Impact. You have plain values going in as props, becoming signals in Impact. And you have signals going out of Impact, consumed as plain values in React.
So with react-query you would be able to just use that data fetching hook and pass data to a StoreProvider, where it becomes a signal. So you could have a store like:
React will update that signal whenever react-query causes reconciliation and the value changed.
In case of RxJS I was thinking of doing something like:
```
function toSignal(rxJsObservable, initialValue) {
const value = signal(initialValue)
const subscription = rxJsObservable.subscribe(value)
cleanup(() => subscription.unsubscribe())
return value
}
```
So basically you give a signal a default value and subscribe to the updates, updating the signal. But as I understand RxJS there are several ways to think about these observables... hot/cold... single event, multiple events etc. etc. Not sure how to create a single abstraction over that.
> Doing this made me realise that it creates this strong concept that StoreProviders becomes the bridge between the reconciling world of React and the observable world of Impact
Agreed, this feels quite intuitive to me as well.
For rxjs & co I was trying to come up with a solution that could incorporate Suspense as well. Something like:
but I'm not happy about constructing a promise and then constructing another promise for each subsequent emission. At least https://github.com/tc39/proposal-promise-with-resolvers will help with with the first issue. Apart from that, I would consider this an acceptable solution - it's not much different from the useState+useEffect dance that you would have to do to integrate with React.
Hot/Cold doesn't really matter here. If it's cold, it will begin emitting as soon as we execute this function. If it's hot, upstream is responsible for deciding when to send & stop sending.
Side-note: I'm not actually trying to use rxjs in any of my current frontend projects. But the ubiquity of this observable pattern means that if you can integrate rxjs, you can probably integrate anything.
Ah, I see! I have not worked much with RxJS observables, though I did implement dark/light mode on their previous website... and wrote some docs on how to use them for state management :-D
But as I understand here you basically want the stream to represent an async value (promise), which makes a lot of sense to me. I have been in this promise challenge before and was not aware of the proposal, and already at stage 4! Great stuff!
As signals are so low level, but has this first class async nature, I wanted to do some iterations on abstractions. Not to necessarily include in Impact, but as you are doing now... just see what kinds of abstractions makes sense on top of it. This was very inspirational, thank you!
It is kinda funny how promises can easily be thought of as kind of a "transport for a state value". When it resolves you move that value into your state primitive of choice. But now the promise is not "a transport for a state value", it IS the state value. And suspense just emphasises that perspective. It is very interesting.
Anyways, thanks again for your perspectives and input. For me it is as much getting the perspectives and terminology right, as the implementation itself :)
9 comments
[ 2.8 ms ] story [ 33.8 ms ] threadThe library has an interesting approach that might work, but I have some reservations:
1. subscribing with the using keyword feels scary as I don't have a conceptual model of what's going on. Why isn't useEffect enough here?
2. As I understand, stores can be initialised with props, but how would I go about passing dynamic values into the store? For instance, what if I want to pass a react-router search param into the StoreProvider whenever it changes?
3. an issue that I have with all state management solutions is that, when you wan't to integrate with another state-manager like react-query, you need to fall back to hooks as the lowest-common denominator. Maybe it would help if Signals supported Observables in addition to promises, so that we can build a
4. related to 2 and 3, I often hold my state in the url or in local storage. A universal state management solution would need to integrate with these external sources. Otherwise, much of the derived state will be computed in the components rather than the stores.
1. So with observation an `ObserverContext` is created when using a store in a component. This "ObserverContext" tracks what signals you access in the component. This "ObserverContext" has to end its tracking when the component function exits. This is what `using` does. It runs logic when the component function exits to stop tracking signals. Then indeed we run a `useEffect`, or rather `syncExternalStore`, subscribing to the `ObserverContext` created. Traditionally you would have to wrap your component function in a higher order component, but then you have an additional import and you have to define your components as variable assignments. This also affects debugging as your components are typically not named with this signature. So yeah, think of `using` as "tracking signal access in the component scope" :)
2. So until now I have not been in a scenario where you would need to do this. Either you use a "key" on the `StoreProvider` to unmount and mount with new props. Which makes sense if the prop for example represents a user or a ticket in a project management app. Typically subscriptions created internally in the store would be its dynamic nature. That said, this does bring up a very interesting idea. We could make the props signals. So you would access the props in a store like any other signal you define in the store. I need to reflect a bit more on it and create a POC, but it is something we should implement before the official release as it would be a breaking change later. Thanks a bunch for this perspective!
3. A bit of text was lost there, but as I understand you suggest giving first class support for traditional Observables in signals? And you mean RxJS types of observables? I did reflect on this early on. Like Angular has a `toSignal` on their observables. This is something that can easily be added later given that we find a good way to execute on it. I'll experiment some more on this, thanks for bringing it up :)
4. Impact is considered a much lower level API than traditional global state management. So if you want to synchronise a signal with a url or local storage you would currently have to do that manually. But as a lower level API I hope to see people build abstractions. For example there could be a local storage sync API that looks like:
``` function MyStore() { const count = signal(0) const flag = signal(false)
} ```In this case the `syncLocalStorage` would be a two way sync with local storage. You define the initial signals and then any changes to them will be synced to local storage. Any changes in local storage is synced back to them, including any initial value. When the store unmounts, the sync is disposed of.
But yeah, I am not sure to what degree these things should be added to the library (It adds maintenance cost which I have been hurt by before). I want to first see if the community sees these opportunities and creates abstractions :)
Again, thanks a bunch for the feedback, I hope my responses made sense and have a great day!
3. I was thinking of any arbitrary event source that supports the typical subscribe/unsubscribe pattern. Either push based like rxjs or pull based like Reacts useSyncExternalStore. rxjs would probably require some extra work to integrate error handling, and I prefer that the pull based approach is just generally more friendly to debugging, tracing & stack traces.
4. That example looks reasonable to me, although we're now once again running into the problem of starting with uninitialised state and only later populating it. In the case of local storage it's not much of a problem since you need a default-value anyways, but what if we try to derive data from react-query? I agree that the framework itself should be agnostic of the actual storage method though.
2, 3 and 4 are all different facets of the same requirement: An observability solution must be able to derive from any external source, or we are back to doing most of our derivations inside react hooks. At least that's the issue I always ran into when dealing with redux, react-query & co. I would have 40% of my state in redux, 40% in react-query, 10% in the URL, 10% in local storage. Then I would do most of the heavy transformations with react hooks, because that's the only place which allows me to combine all sources. Im aware that some of this could be solved with redux middleware / rtk-query, but then you're once again loosing type-safety due to every component needing to handle the uninitialised case.
You were absolutely right in saying that observability is better than reconciliation for state management. However, I think this does not only apply to state management but to all types of derived data, including server state, local storage etc...
Yes, what you are saying makes a lot of sense. And traditionally you would need to do some combination of a global state store like Redux, react-query and custom hooks.
Impact does support a new type of asynchronous pattern with React, which I am very excited about. It is supported in React 18 using the polyfilled `use` hook in Impact, but with React 19 you can use the `use` hook of React itself. What this means is that you do not have to differentiate sync/async values in signals. Signals makes promises observable. So:
const data = signal(fetchData()) const count = signal(0)
Both are observable values, where the data promise can be consumed in components with the `use` hook or by checking its data.status property directly.
So instead of having to split your state management between redux, react-query and hooks you are now able to just think state and co locate it with the same primitives and abstractions using stores.
If you read the https://impact-react.dev/deep-dive/queries-and-mutations docs, you'll see how the low level usage of this is. I would expect someone to create a similar data store abstraction for Impact like react-query, to manage expiration etc. at a higher level, like:
const DataStore = createDataStore(...)
which allows you to:
function MyComponent({ id }) { using dataStore = useDataStore()
}But unlike react-query it would be based on observable promises.
But yeah, I do not want to state that Impact should just "do everything", but it is a low enough abstraction to co locate all state management in the same abstraction, if that is a priority. It might still feel too low level at the moment, but I am very curious to see where it goes.
When it comes to RxJS types of observables it is possible to do the same as `toSignal` in Angular, but I am unsure if it should be a first class concept like promises. Maybe it should, but also something the community can build :)
Would love for you to read https://impact-react.dev/deep-dive/queries-and-mutations and give your thoughts. We refactored a part of our own application where we needed to:
1. Fetch data about the content (promise) 2. Use that to instantiate a process (promise) 3. But async instantiation of this process also emitted events during instantiation we wanted to display as part of loading the process 4. Finally mount state management for this data bound to the process
Previously these steps where multiple components, but with this new async pattern we where able to express it all in a single component, line by line, using a store. It blew my mind :)
Requiring everything to be based on your low-level implementation seems like a tall ask. One thing that I love about React is how easily it can be configured to read from any reactivity-solution. This makes it possible to integrate pre-existing code or libraries. In impact, I see two possible clean entry points for external state: the Provider props, or a signal constructor similar to useSyncExternal store.
I did some experimentation to see if I can write a function that converts an rxjs stream to a signal, but it involved multiple dirty hacks such as extracting the resolve/reject functions from the promise callback.
I just published a version with the signal props. Doing this made me realise that it creates this strong concept that StoreProviders becomes the bridge between the reconciling world of React and the observable world of Impact. You have plain values going in as props, becoming signals in Impact. And you have signals going out of Impact, consumed as plain values in React.
So with react-query you would be able to just use that data fetching hook and pass data to a StoreProvider, where it becomes a signal. So you could have a store like:
```
function MyStore(props) {
}```
React will update that signal whenever react-query causes reconciliation and the value changed.
In case of RxJS I was thinking of doing something like:
```
function toSignal(rxJsObservable, initialValue) {
}```
So basically you give a signal a default value and subscribe to the updates, updating the signal. But as I understand RxJS there are several ways to think about these observables... hot/cold... single event, multiple events etc. etc. Not sure how to create a single abstraction over that.
Agreed, this feels quite intuitive to me as well.
For rxjs & co I was trying to come up with a solution that could incorporate Suspense as well. Something like:
but I'm not happy about constructing a promise and then constructing another promise for each subsequent emission. At least https://github.com/tc39/proposal-promise-with-resolvers will help with with the first issue. Apart from that, I would consider this an acceptable solution - it's not much different from the useState+useEffect dance that you would have to do to integrate with React.Hot/Cold doesn't really matter here. If it's cold, it will begin emitting as soon as we execute this function. If it's hot, upstream is responsible for deciding when to send & stop sending.
Side-note: I'm not actually trying to use rxjs in any of my current frontend projects. But the ubiquity of this observable pattern means that if you can integrate rxjs, you can probably integrate anything.
But as I understand here you basically want the stream to represent an async value (promise), which makes a lot of sense to me. I have been in this promise challenge before and was not aware of the proposal, and already at stage 4! Great stuff!
As signals are so low level, but has this first class async nature, I wanted to do some iterations on abstractions. Not to necessarily include in Impact, but as you are doing now... just see what kinds of abstractions makes sense on top of it. This was very inspirational, thank you!
It is kinda funny how promises can easily be thought of as kind of a "transport for a state value". When it resolves you move that value into your state primitive of choice. But now the promise is not "a transport for a state value", it IS the state value. And suspense just emphasises that perspective. It is very interesting.
Anyways, thanks again for your perspectives and input. For me it is as much getting the perspectives and terminology right, as the implementation itself :)