facebookexperimental / Recoil

Recoil is an experimental state management library for React apps. It provides several capabilities that are difficult to achieve with React alone, while being compatible with the newest features of React.
https://recoiljs.org/
MIT License
19.57k stars 1.18k forks source link

Atom effect able to get another atom's value #707

Closed mdlavin closed 2 years ago

mdlavin commented 3 years ago

I've been looking into atom effects and they look really helpful. In the example, it shows a state synchronization example effect like this:

const syncStorageEffect = userID => ({setSelf, trigger}) => {
  // Subscribe to remote storage changes and update the atom value
  myRemoteStorage.onChange(userID, userInfo => {
    setSelf(userInfo); // Call asynchronously to change value
  });

  // Cleanup remote storage subscription
  return () => {
    myRemoteStorage.onChange(userID, null);
  };
};

That looks perfect, if myRemoteStorage is a static, but how would you structure that same effect when myRemoteStorage is coming from a Recoil value? If the datastore is also tracked in a Recoil atom, then I can't figure out a good pattern for using it in an effect. Was there an approach in mind to handle that pattern?

smith-chris commented 3 years ago

I'm also looking for a way to subscribe to a recoil value in an atom's effect now. Will let you know if I find a way but also pls share your solution if you'll find any

BenjaBobs commented 3 years ago

Since an atom doesn't expose a get function, I guess the only way would be to catch a get function elsewhere and store it in a variable outside of Recoil. Then you'd be able to use it, but it is a bit hacky, and I don't think it's the intended usage. I'm also not sure the implications it would have on concurrent rendering.

smith-chris commented 3 years ago

So I found the solution, in my case its not bi-directional but I imagine this could be achieved with the set without a problem. Interesting thing here is you'd expect that using this selector in multiple places would cause multiple fetches but it doesn't, which is perfectly what I wanted :) gotta love recoil Also the result is memoed and so changing activeUsernameState in recoil to already previously used username also doesn't cause refetch, which is again what I wanted. Here's the example:

export const suggestedFiltersSelector = selector<SuggestedFilters>({
  key: 'suggestedFiltersSelector',
  get: async ({ get }) => {
    try {
      const { data } = await apolloClient.query<
        IGetSuggestedFiltersQuery,
        IGetSuggestedFiltersQueryVariables
      >({
        query: GetSuggestedFiltersDocument,
        variables: { username: get(activeUsernameState) },
      });

      return data?.getSuggestedFilters.suggestedFilters;
    } catch (error) {
      Logger.error(error);
    }
    return {};
  },
});

Hope that also helps you :)

drarmstr commented 3 years ago

@mdlavin - We are exploring the ability for atom effects to look at the state of other atoms, perhaps via providing a mechanism like getSnapshot() as a callback for the atom effect.

justintoman commented 3 years ago

What about looking at the state of the current atom? Is there a way to find the old/current value in onSet before the new value is committed? Or the initial value in the case that trigger === 'set'? Speaking of which, what is the purpose of the node self reference in the effects? It doesn't seem like I can use it for anything.

drarmstr commented 3 years ago

The onSet() callback is provided two parameters, the new value and the old value. Though, the API is still in flux and may need to be tweaked to fully support async/error states.

The node allows you to do things like build up a list or map of atoms which can be used by other async callbacks.

likern commented 3 years ago

I'm coming from Jotai competitor. Looked at how RecoilJS can solve my problems.

It looks like atom affects is what Jotai is. Jotai doesn't have selector entity, only atoms. Moreover atoms can get and set another atoms and can even get it's own value.

My use case - use atoms as source of truth and update itself as fast as possible, while updating database is considered as side-effect.

export const tagsAtom = atom(
  (_) => {
    const tagRepository = getTagRepository();
    return tagRepository.find();
  },
  (
    get,
    set,
    { id, ...newTag }: QueryDeepPartialEntity<Tag> & { id: string }
  ) => {
    const oldTags = get(tagsAtom);
    const newTags = oldTags.map((oldTag) => {
      if (oldTag.id === id) {
        return {
          ...oldTag,
          ...newTag
        };
      } else {
        return oldTag;
      }
    });

    // @ts-expect-error
    set(tagsAtom, newTags);

    const tagRepository = getTagRepository();
    tagRepository.update({ id }, newTag);
  }
);

how this can be achieved using RecoilJS? I've read all documentation and still can't figure that out.

vaclavgreif commented 3 years ago

@drarmstr Any updates on this one? I might be missing something, but I find it close to impossible to update another atom value when some atom value changes. For now I used the selector similar to this: https://github.com/facebookexperimental/Recoil/issues/707#issuecomment-721050102, but is there any easier way to subscribe to a specific atom changes and update other atoms?

zfanta commented 3 years ago

I recommend using family function to get another atom's value in effect.

In component.

const a = useRecoilValue(aState)
const b = useRecoilValue(bState({ a }))

Recoil definition.

const bState = atomFamily({
  key: 'bState',
  default: undefined,
  effects_UNSTABLE: ({ a }) => [
    function loadSubscriptions ({ setSelf }) {
      const result = getSomeValueFromA(a)
      setSelf(result)
    }
  ]
})
gursesl commented 3 years ago

Ability to grab other atoms' values inside the effect functions would be very helpful. We need to rehydrate the local state from a remote Firebase collection based on a number of query parameters, already part of various atoms. It wouldn't make much sense to create an atom family when a simple get parameter to the effect functions would suffice. Am I thinking this incorrectly?

dsboo commented 3 years ago

also i think it's a good feature if it possible that in effects_UNSTABLE.onset, update other atom. i want some atom's effect propagation to other atom's effect.

drarmstr commented 2 years ago

Proposed solution in #1205 and #1210

nikhilag commented 2 years ago

@drarmstr Any idea when this feature will go live for adding getLoadable() and getPromise() in atom effects? This will be quite useful in a problem I am currently facing.

drarmstr commented 2 years ago

They have already landed in the main branch and so should be available with the nightly build branch.

stefanstaynimble commented 2 years ago

I believe there is a breaking issue, I am getting cleanup is not a function when unmounting a component accessing an atom using getPromise in one of the effects_UNSTABLE.

effects_UNSTABLE: [
  async ({getPromise, onSet}) => {

    const sdk = await getPromise(SDKState);
    const CancelToken = axios.CancelToken;
    const source = CancelToken.source();

    onSet((newId) => {
      sdk.post("/api/some-api-call", {
        data: {
          key: "value"
        },
        cancelToken: source.token
      });
    });

    return () => {
      source.cancel("Operation canceled due to cleanup.");
    };
  }
]

Is there something I am doing wrong?

stefanstaynimble commented 2 years ago

So I figured out the issue above was due to having a async function in the effects array.

njarraud commented 2 years ago

Are there any plans to implement a function to update other atoms inside onSet?

0xF013 commented 2 years ago

My case is something like this: I would like to reset an atom when another one changes, i.e. I have an atom for the current categoryId. I would like to reset the projects atom when categoryId changes and I would like to be able to do it without an effect in react somewhere, otherwise I am having an implicit dependency on that react hook. I could use the effect of the categoryId but then that atom will have to know of all the atoms that depend on it, which is kinda the inverse of what I am looking for.

So, any news on this?

drarmstr commented 2 years ago

Closing issue as this feature is now released with 0.5.

For the pattern of resetting another atom when a category changes you may wish to consider using an atom family pattern as a "scoped atom" -- Use the results of the category as the parameter for the family. See this example

taylorcode commented 1 year ago

I think there are valid use cases for calling get inside of an atom effect. Example:

To work around this limitation, one can create a selector and which performs const arg = get(atomA) and then passes this value to the atom which has the effect get(atomFamilyWithEffect(arg)) but this is problematic because this will create new atoms every time the value of arg changes, which will cause a memory leak if arg is highly dynamic (such as based on user input). Perhaps this problem can be solved by providing a way to indicate that a given atom within an atom family is unused and can be safely GC'd, but https://github.com/facebookexperimental/Recoil/issues/366 suggests that this problem has not been solved.