Recoil is an experimental state management library for Reactjs made by Facebook developers. It solves the issues faced by developers using just React's state management features.
Why use Recoil?
- ⭐ Minimal and Reactish : Recoil works and thinks like React. Add some to your app and get fast and flexible shared state.
- Cross-App Observation : Implement persistence, routing, time-travel debugging, or undo by observing all state changes across your app, without impairing code-splitting.
- Data-Flow Graph : Derived data and asynchronous queries are tamed with pure functions and efficient subscriptions.
If you want to learn more about the motivation behind Recoil, then check out this video.
What's an Atom?
An atom represents a piece of state. Atoms can be read from and written to from any component. Components that read the value of an atom are implicitly subscribed to that atom, so any atom updates will result in a re-render of all components subscribed to that atom:
If you are not familiar with Recoiljs, then see this. (Don't worry it's very short!)
Now that we are on the same page, let's implement Atom of Recoil using just Javascript and Reactjs!
What I cannot create, I do not understand - Richard Feynman
🛠️ Building Atom
We'll define Metadata of Atom as an interface that has the following properties.
key
property of type string, which has to be unique with respect to other atoms.default
property, which is the default/initial state of the atom.
Example,
const textStateMetadata = ({
key: "I am unique",
default: "React is Awesome!"
})
Now, that we have something that represents an atom, we have to persist the state of an atom which will be shared by all the components subscribing to it.
One way to do it is by having the state of all the atoms near the root of the React tree and providing it to the children via context API. Let's call thisRecoilRoot
.
import React, { createContext } from "react";
const RecoilContext = createContext({ state: {}})
function RecoilRoot({children}) {
return <RecoilContext.Provider value={{ state: {} }}>
{children}
</RecoilContext.Provider>;
}
It can be used like
const App = () => {
return (
<RecoilRoot>
{/* Children Components */}
<SomeChildComponent />
</RecoilRoot>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
Now that we have a place to store the state of the atoms, let's start implementing a React hook to consume and modify the atom state (useRecoilState
).
function useRecoilState(atom) {
// state is a object with atom.key as the key and the atom's
//state as the value for each atom subscribed.
const {state} = useContext(RecoilContext);
if(typeof state[atom.key] === "undefined") {
// The state object doesn't have the state of this atom. So, let's add it!
state[atom.key] = atom.default;
}
// Each component using this hook, has its own local copy of the state of the atom.
const [atomState, setLocalState] = useState(state[atom.key])
return [atomState, setLocalState];
}
Now, this hook only provides a way to initially provide the local state of the atom equal to the default state of the atom and there is no way to have all the components subscribing to the same atom have the same state after updates.
Let's solve these issues.
const [atomState, setLocalState] = useState(state[atom.key]);
const setAtomState = (nextStateOrFunction) => {
// React provides two ways to update state in the useState hook, so we'll stick with the same API.
// nextStateOrFunction could be either the next state value
// or a function which takes the previous state and returns the next state.
const currentState = state[atom.key];
let nextState;
if (typeof nextStateOrFunction === "function") {
// nextStateOrFunction is a function.
nextState = nextStateOrFunction(currentState);
} else {
// nextStateOrFunction is the next state value.
nextState = nextStateOrFunction;
}
// Update the next state value
state[atom.key] = nextState;
// Triggers re-render of the component using this hook
// and updates the local state of the atom in that component to match the common global state.
setLocalState(nextState);
};
// Note, This time we return setAtomState and not setLocalState!
return [atomState, setAtomState];
Now our hooks can update the global state of the atom on updates and also we have it in sync with the local copy of the atom state. 🎉
Still, there is a problem, if a component updates the atom state, we have to update the local state of the atom in all components subscribing to it. (Currently, we only update the local state of the component modifying the atom state).
One way to solve it is by emitting and listening to events.
We can emit an event on the atom state update with an event name unique to each atom.
To get a unique event name we'll prepend "_ATOM_STATE_UPDATE_"
to the key of the atom.
If you're not familiar with DOM events then check out this.
const dispatchAtomStateChangeEvent = () => {
// Creates a event.
// prepend "_ATOM_STATE_UPDATE_" to make the event name unique.
//
const event = new Event("_ATOM_STATE_UPDATE_" + atom.key); // Each event is uniquely identified by the name being passed to the constructor.
// emits the event on the window element.
window.dispatchEvent(event);
}
Also, listen to that event and update the local copy of the atom state on that event.
useEffect(() => {
window.addEventListener("_ATOM_STATE_UPDATE_" + atom.key, () => {
// update the local state to match the global atom state.
setLocalState(state[atom.key]);
})
// runs only once (on the component mount). we add the event listener on mount.
}, [])
Combining all 🚀
We have two components subscribing to textAtom
. Clicking the button modifies the atom state and you can see that it reflects in both the components!
If you made it till the last, congrats 🎉