question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Using dispatch pattern with Recoil

See original GitHub issue

Hello, I’m trying to create an abstraction layer on top of Recoil for my app. I would like state changes to be triggered by dispatching actions (like in Redux), while keeping the benefits of the atomic model (only updating components that are explicitly subscribed to an atom).

This works fine for atoms, but I can’t figure out how to make it work for atomFamilies.

Let’s say I have an atom, foo, and I can set its value via an action, "setFoo":

import {atom, useSetRecoilState} from "recoil";

const foo = atom({
    key: "foo",
    default: "Hello, I'm foo"
});

const useDispatch = () => {
    const setFoo = useSetRecoilState(foo);

    return (action, data) => {
        switch(action){
            case "setFoo":
                setFoo(data);
                break;
        }
    };
};

Now, I can call this useDispatch hook once at the top level of my app, and pass the dispatch function it returns down (perhaps using Context, since the function itself never changes). When a "setFoo" action is dispatched, the hook sets the new value of the foo atom, and any components that are subscribed to foo will rerender. So far so good.


Now, let’s make foo an atom family, instead of just an atom:

import {atomFamily, useSetRecoilState} from "recoil";

const foo = atomFamily({
    key: "foo",
    default: "Hello, I'm foo"
});

const useDispatch = () => {
    const setFoo = useSetRecoilState(foo(/* ??? */)); // Atom identifier needs to be known here

    return (action, data) => {
        switch(action){
            case "setFoo":
                setFoo(data);
                break;
        }
    };
};

The trouble is, the atom identifier needs to be known at the time when useDispatch is called. I can’t call useSetRecoilState inside the body of the dispatcher to dynamically create the setter, since that would break the rules of hooks.


Here’s my proposal:

What if useSetRecoilState and related hooks were able to accept atom families, instead of just atoms? Then, the setter it returns could take an atom identifier as an argument, and I could do something like this:

import {atomFamily, useSetRecoilState} from "recoil";

const foo = atomFamily({
    key: "foo",
    default: "Hello, I'm foo"
});

const useDispatch = () => {
    const setFoo = useSetRecoilState(foo); // Atom *family* is passed in, instead of an atom

    return (action, data) => {
        switch(action){
            case "setFoo":
                setFoo(data.id, data.value); // Setter takes atom identifier as an argument
                break;
        }
    };
};

If not, are there any other ways to implement this pattern?

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:5 (2 by maintainers)

github_iconTop GitHub Comments

1reaction
tpictcommented, Jan 10, 2022

Here’s the solution I’m using:

export interface DispatchWithThunk<A> {
  (action: A): void | ((dispatch: Dispatch<A>) => void);
}

export function useRecoilDispatch<A>(
  fn: (callbackInterface: CallbackInterface) => DispatchWithThunk<A>,
): Dispatch<A> {
  const dispatch: Dispatch<A> = useRecoilCallback(callbackInterface => {
    const handler = fn(callbackInterface);
    return action => {
      const thunk = handler(action);
      if (typeof thunk === "function") {
        thunk(dispatch);
      }
    };
  }, []);

  return dispatch;
}

In usage:

useRecoilDispatch(({ set, snapshot }) => action => {
  switch(action.type) {
    case "DO_SOME_WORK":
      set(statusAtom, "Working!");
      return async dispatch => {
        const data = await grabSomeData();
        dispatch({ type: "WRITE_DATA", data });
      }
    case "GIVE_UP_WORK":
      set(statusAtom, "Cancelled");
    case "WRITE_DATA":
      const status = await snapshot.getPromise(statusAtom);
      if (status === "Cancelled") {
        // Don't write data, user cancelled
        return;
      }

      set(dataAtom, action.data);
  }
});

By returning a callback that receives dispatch as an argument, you read the latest state snapshot, which is handy when doing async work.

1reaction
drarmstrcommented, Jan 10, 2022

Thanks @mondaychen. You could also use useRecoilTransaction_UNSTABLE() with the reducer pattern.

Or you could make a universal setter callback:

const setAtom = useRecoilCallback(({set}) => (node, value) => set(node, value));
Read more comments on GitHub >

github_iconTop Results From Across the Web

Refactoring a Redux app to use Recoil - LogRocket Blog
Get hands-on with Facebook's nascent Recoil library by refactoring a Redux app and comparing their differences in implementation.
Read more >
recoil.dispatch JavaScript and Node.js code examples - Tabnine
Best JavaScript code snippets using recoil.dispatch(Showing top 3 results out of 315) · Most used recoil functions · Popular in JavaScript.
Read more >
Using Recoil instead of Redux For State Management In ...
Redux is not the only State management alternative for React. Learn how to use Recoil to save development time.
Read more >
Asynchronous Data Queries - Recoil
Recoil allows you to seamlessly mix synchronous and asynchronous functions in your data-flow graph of selectors. Simply return a Promise to a value...
Read more >
What is Recoil exactly? - DEV Community ‍ ‍
Redux follows the Flux architecture which makes use of actions, dispatches, reducers and stores. Data flows from actions, into a dispatcher ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found