[Proposal] Recoil Atom Actions
See original GitHub issueLet’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:
- the possibility to make an isolated data layer
- 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:
- Created 3 years ago
- Reactions:4
- Comments:13 (4 by maintainers)
Top GitHub Comments
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.
@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: