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.

[Proposal] Recoil Atom Actions

See original GitHub issue

Let’s consider such kind of situation: we have a theme in our application, and we want to have a shared state for that. With Recoil it’s easier than ever before:

import { atom } from 'recoil';

const themeState = atom({
  key: 'themeState',
  default: 'dark',
});

That’s great! Now two more things about further usage of the theme.

In our application, we will toggle theme and the current state of the theme should be synchronized with the localStorage (the initial value should be taken from the localStorage if there is one)

Okay, we can change our code a little bit:

import { atom } from 'recoil';

const themeState = atom({
  key: 'themeState',
  default: localStorage.getItem('theme-mode') || 'dark', // it's not so good, and should be updated in the future
});

Now, this atom can be used in different components, and inside those components, toggle can be produced. For example:

SomeComponent.js

import { useRecoilState } from 'recoil';
import { themeState } from '...';

import { themePair } from 'config';

function SomeComponent() {
  const [theme, setTheme] = useRecoilState(themeState);

  function handleThemeToggle() {
    const mode = theme === themePair[0] ? themePair[1] : themePair[0];
    setTheme(mode);
    localStorage.setItem('theme-mode', theme);
  }

  // ...

  return ... ;
}

The big problem here, in my opinion, is the unpredictability of the state updates and the lack of “structurdness”. Our atom can be used in all components, and the value can be changed whether a developer wants. The state changes are not predictable. To handle this I created a single entry point for each shared state. In the case of theme it looks like this:

store/theme/index.js

import { atom, useRecoilState } from 'recoil';

import { themePair } from 'config';

const themeState = atom({
  key: 'themeState',
  default: localStorage.getItem('theme-mode') || 'dark',
});

function useTheme() {
  const [theme, setTheme] = useRecoilState(themeState);

  function toggle() {
    const mode = theme === themePair[0] ? themePair[1] : themePair[0];
    setTheme(mode);
    localStorage.setItem('theme-mode', theme);
  }

  return [theme, { toggle }];
}

export default useTheme;

Now, the interaction with the theme will look like this:

SomeComponent.js

import { useTheme } from 'store/theme';

function SomeComponent() {
  const [theme, actions] = useTheme();

  function handleThemeToggle() {
    actions.toggle();
  }

  // ...

  return ... ;
}

This looks much much better. Now, the shared state and the possible interactions with it are isolated; it’s more predictable and testable. But still not the ideal.

Effects

Recently “Atom Effects” has been announced. That’s a really great feature and gives us some important opportunities. In our case, we can handle the synchronization with localStorage. Let’s change our code a little bit.

import { atom, useRecoilState } from 'recoil';

import { themePair } from 'config';

const themeState = atom({
  key: 'themeState',
  default: 'dark',

  effects: [
    ({ setSelf,  }) => {
      localStorage.getItem('theme-mode') && setSelf(localStorage.getItem('theme-mode')); // the initial value

      onSet(value => localStorage.setItem('theme-mode', value));
    },
  ],
});

function useTheme() {
  const [theme, setTheme] = useRecoilState(themeState);

  function toggle() {
    const mode = theme === themePair[0] ? themePair[1] : themePair[0];
    setTheme(mode);
  }

  return [theme, { toggle }];
}

export default useTheme;

Actions

My proposal is to add Atom Actions; very similar to Atom Effects. It’s an optional field in an atom and can define the possible interactions with the atom’s state. If actions field is not empty useRecoilState will return [state, actions] pair instead of [state, setState]. If actions are not defined everything will work without any change. Let’s change our code according to this:

import { atom, useRecoilState } from 'recoil';

import { themePair } from 'config';

const themeState = atom({
  key: 'themeState',
  default: 'dark',

  effects: [
    ({ setSelf,  }) => {
       localStorage.getItem('theme-mode') && setSelf(localStorage.getItem('theme-mode')); // the initial value

    onSet(value => localStorage.setItem('theme-mode', value));
    },
  ],

  actions: {
    toggle({ set }) {
       set(value => value === themePair[0] ? themePair[1] : themePair[0]);
    }
  },
});

export default themeState;

And it can be used like this:

SomeComponent.js

import { useRecoilState } from 'recoil';
import { themeState } from 'store/theme';

function SomeComponent() {
  const [theme, actions] = useRecoilState(themeState);

  function handleThemeToggle() {
    actions.toggle();
  }

  // ...

  return ... ;
}

The benefits of this are:

  1. the possibility to make an isolated data layer
  2. predefined actions for the atom 2.1) predictable state updates 2.2) readability

As we saw, we can do something like this with a custom hook (in this case useTheme), but in my opinion, it wasn’t the best, purest, and clearest way. The person who is responsible for the data layer can define all possible shared states for the application with their possible updates without interacting with hooks or other specific tools; it’s pure js - just define your state, effects, and actions. Based on that actions and effects, we can make a graph of possible updates, we can easily test and debug, and what is the most important thing we can keep everything predictable.

The above example is a real-life example, but there is just a single action. I think the difference can be more expressive in more complex examples.

It’s just a week I started working with Recoil, and maybe there are better patterns for organizing shared states that I don’t know. So, I am glad to hear your feedback. Thank you!

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:4
  • Comments:13 (4 by maintainers)

github_iconTop GitHub Comments

4reactions
BenjaBobscommented, Sep 18, 2020

I’m still chewing on the details, but I will when if I come up with something that I feel can be useful in a general sense without being too opinionated/locked. Maybe you should have a section in the documentation where you collect common partterns/ways of using Recoil. I feel especially organizing one’s data in a scaleable way both regarding discovery and cluttering is something that could use a good thinking about, and maybe a guide.

3reactions
drarmstrcommented, Sep 14, 2020

@suren-atoyan - We don’t want to change the type of the return value of the useRecoilState() dynamically based on optional atom properties (with the potential exception of readable/writeable selectors). This would make it very difficult for type systems to correctly track the types, especially when it may be dynamic which atom/selector is actually being used in the hook.

It’s perfectly reasonable to export a set of hooks/callbacks for your data layer to abstract actions for working with your Recoil state. This helps you enforce type-safety for your custom actions.

Also, fyi, for a toggle operation it’s best to use an updater form of the setter to ensure you are toggling the current state:

function useTheme() {
  const [theme, setTheme] = useRecoilState(themeState);

  function toggle() {
    setTheme(currentTheme => currentTheme === themePair[0] ? themePair[1] : themePair[0]);
  }

  return [theme, { toggle }];
}
Read more comments on GitHub >

github_iconTop Results From Across the Web

Build your own Recoil - ITNEXT
Recoil is a new experimental state management library for React provided by Facebook. The core concept of it is Atoms and Selectors: With ......
Read more >
selector(options) - Recoil
Recoil manages atom and selector state changes to know when to notify components subscribing to that selector to re-render. If an object value...
Read more >
CHEMICAL ACTION PRODUCED BY RADON V. REVISION ...
I t was previously shown by one of us2 that the recoil atoms from alpha- radiation of radon (and of its decay products)...
Read more >
Newest 'recoiljs' Questions - Page 3 - Stack Overflow
I have a recoil state that is an object that is structured like this: const Proposal = atom({ key: "PROPOSAL", scopes: [{ assemblies:...
Read more >
Refactoring a Redux app to use Recoil - LogRocket Blog
You may be surprised to see the direction the Recoil app takes. While Redux focuses on a flow of state from actions to...
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