Recoil: Subscribing effects to state

Created on 16 May 2020  路  9Comments  路  Source: facebookexperimental/Recoil

I'm wondering if there's plans to allow functionality such as subscribing effects to state. Let me be a bit more specific in what I mean - I'm trying to store a certain atom's value to localstorage, and right now the best solution I've come up with is this:

Declaring my atom like so:

const myState = atom({
  key: 'myState',
  default: JSON.parse(window.localStorage.getItem('myState')) || myDefaultValue,
});

And writing this in my root component:

const myStateValue = useRecoilValue(myState);
useEffect(() => {
  window.localStorage.setItem('myState', JSON.stringify(myStateValue));
}, [myStateValue]);

But it would be really nice if we could specify some kind of subscribed function somewhere that just runs the localstorage setter on value change, so we don't have to rely on the render of the root component. As an example API:

const myState = atom({
  key: 'myState',
  default: JSON.parse(window.localStorage.getItem('myState')) || myDefaultValue,
  subscriber: value => window.localStorage.setItem('myState', JSON.stringify(value)),
});

If this functionality already exists and I'm just missing it, please feel free to point it out and close the issue. Otherwise I'd love to hear the team's thoughts on such functionality!

enhancement

Most helpful comment

There is a useTransactionObservation_UNSTABLE hook you can use to subscribe to state changes for managing persistence. There is also a persistence_UNSTABLE option for atoms to add metadata, such as a validator function. It's listed as _UNSTABLE as we're still finalizing the API, but we use this internally. We're working now to clean up this API and publish an example persistence library.

All 9 comments

There is a useTransactionObservation_UNSTABLE hook you can use to subscribe to state changes for managing persistence. There is also a persistence_UNSTABLE option for atoms to add metadata, such as a validator function. It's listed as _UNSTABLE as we're still finalizing the API, but we use this internally. We're working now to clean up this API and publish an example persistence library.

As Douglas said we have an API that you can use for this currently, albeit it's a bit half-baked and we're planning a much better and more flexible version soon. In the meantime, check out useTransactionObservation and the initializeState prop of RecoilRoot (unlike what you have with default there this doesn't depend on module evaluation timing.)

If you just want an effect to be triggered by changes to a specific atom, you can also have a component with useEffect.

I think there still needs to be a way to subscribe to individual atoms instead of relying on only useTransactionObservation_UNSTABLE, since one of the main points of using recoil instead of redux is to avoid triggering ALL unrelated subscribers at every single change in the app state.

@Qrysto @Joonpark13 - Yup, stay tuned for "Atom Effects" coming soon...

Having looked at the new "Atom Effects", there still doesn't seem to be a simple way to alter the value of one atom when another's state changes. Perhaps you envisage selectors for that, but I've yet to see (or figure out) a way to make that work neatly if both states are atoms.

"Atom Effects" has setSelf/resetSelf, but what if I want to setOther/resetOther?

Is there/will there be a way to subscribe to one atom and then set another on change, without going the useEffect route (which just feels clunky)?

This my current approach, but I dislike the way it's separate from my main state code.

DimensionsAtom needs to be reset whenever listingAtom changes.

function StateManager() {
    const listing = useRecoilValue(listingAtom);
    const setDimensions = useSetRecoilState(dimensionsAtom);

    useEffect(() => {
        setDimensions({});
    }, [listing]);

    return null;
}

Contemplating adding some sort of onListen() for an atom effect to listen to other atoms/selectors. But, trying to reason if it's really justified and doesn't encourage bad patterns.

FWIW, I like the idea.

Recoil has allowed me to break down my state to neat, reusable parts. In a complex application, that's a real plus and the code is much cleaner than redux/context versions I've had before.

But there are plenty of occasions where the state logic (separate from the display logic) demands knock-on effects and it feels right to try to wrap those parts of the code in together.

I think a great way to handle it would be to have a type like EffectSubscriber<T> where you would dispatch messages of type T to it and it has the ability to parse the sent data and set atoms and perform effects as needed. It would let the subscriber be ignorant of how it gets messages which means it could even be sent new data from multiple sources (and be less fragile to code changes).

In addition, it would be awesome to have its own state as well. This would really enable a MVU pattern within recoil. So it would be something along the lines of giving it an initial state and like the set field for selectors where it takes the object to perform operations {set: SetRecoilState, get: GetRecoilValue, reset: ResetRecoilState} and the new message T and then returns the new state and any effects to dispatch (like an array of void => void).

Where it would look something like this:

const mySubscriber = effectAtom({
    key: 'mySubscriber',
    default: { someField: 1 },
    update: ({get, set}, msg, state) => {
        if (msg.tag == SomeType.SomeMsg) {
            return { someField: msg.value + state.someField }, []
        }
        else {
            return state, [() => console.log("hello!")]
        }
    }
});

Ideally modifying the state would be done inside the effect array as well to make the update function pure(er), but in either case I think this would be effective.

This is a well established pattern in Elm, F#, and other functional languages as a way for handling effects.

I don't know, but this idea of making the effects lazy might help with concurrent mode compatibility?

Currently, this is how I have implemented a watcher facility for my form.

export const subscribeToFormFieldsSelector = selectorFamily<
  FormFieldAtomType[],
  string[] | undefined
>({
  key: atomKeys.subscribeToFormFieldsSelector,
  get: (fields = []) => ({ get }) => {
    if (!Array.isArray(fields)) {
      fields = [fields];
    }
    let fieldValues: FormFieldAtomType[] = [];
    for (let field of fields) {
      if (typeof field === "string" && field !== "") {
        let fieldState = get(formFieldAtom(field));
        fieldValues.push(fieldState);
      }
    }
    return fieldValues;
  },
});
Was this page helpful?
0 / 5 - 0 ratings

Related issues

Sawtaytoes picture Sawtaytoes  路  4Comments

tklepzig picture tklepzig  路  3Comments

robsoncezario picture robsoncezario  路  3Comments

art1373 picture art1373  路  4Comments

julienJean99 picture julienJean99  路  3Comments