Redux is well-known for state management in React, and as a mass panderer, I am using Redux as usual.
However, there are a few areas where I find it hard to use Redux.
One is that there are many boiler plates anyway.
Just adding or modifying a few states (variables) requires a significant amount of coding.
Another is that Redux is a library for maintaining state and redrawing components that reference state, so it does not have the ability to update state asynchronously, which requires users to implement this on their own.
However, Redux is merely an implementation of the "Flux" state management idea proposed by Facebook, and there is nothing wrong with Redux.
But Redux was not created by Facebook, and I don't think Facebook uses Redux for their services.
Facebook is a professional SPA company that operates such a huge service, and I was hoping that Facebook would create a framework for the entire SPA, including state management.
debut of a new player
In the midst of all this, Facebook has introduced Recoil, a state management library, although it is still in beta.
I've touched it, and I can conclude that it's a good idea.
This is it! What I wanted!
Just write what you want to do, and there are no boilerplates to get fed up with.
Asynchronous updates of state and other situations commonly used in SPA were taken into account and features that seemed necessary were provided.
Below I will write a brief description of Recoil and my impressions of it.
What is Recoil?
Recoil is a React state management library created by Facebook.
You can create a kind of global variable called atom
, and components that use that atom
will subscribe (reference) to atom
.
Then, when atom
is updated, only the component that subscribed to atom
is redrawn.
Multiple components can subscribe to a single atom
at the same time, allowing efficient redrawing of only the necessary components while keeping the state in one place.
Incidentally, Redux creates one huge state tree, but the granularity of atom
is fine-grained and created independently at the variable/object level.
atom
As a sample, let's make an application that ups and downs a counter.
import React from 'react'; import './App.css'; import { RecoilRoot, atom, useRecoilState, } from 'recoil'; const countAtom = atom<number>({ key: `countAtom`, default: 0, }); function UpDown() { const [count, setCount] = useRecoilState(countAtom); return ( <div> <button onClick={() => setCount(count - 1)}>Down</button> {count} <button onClick={() => setCount(count + 1)}>Up</button> </div> ); } function App() { return ( <RecoilRoot> <UpDown /> </RecoilRoot> ); } export default App;
It's so simple that even at a quick glance, you can kind of tell what you're doing.
atom creation
Create atom
to remember the number countAtom
.
The argument is set to a default value and an identification key.
Internally, Recoil uses this identification key, not the variable name, to identify atom
, so the key must be unique.
Subscribe to atom
To subscribe to atom
by a component, specify atom
to be subscribed to by useRecoilState()
.
The current value of atom
and the function to change atom
can then be obtained.
Subscribe from multiple components
The atom
from earlier can be used for other components.
However, as before, you only need to subscribe to atom
in the component you want to use.
function ViewCount() { const [count, setCount] = useRecoilState(countAtom); return ( <div> {count} </div> ); } function App() { return ( <RecoilRoot> <UpDown /> <ViewCount /> <ViewCount /> <ViewCount /> </RecoilRoot> ); }
That's all!
With just this, if a state is referenced by multiple components and that state is changed, only the component referencing it will be redrawn.
The code is clean and easy to read, as no extra description is required, and only the work that you want to do needs to be described.
Separate business logic
Since it is not good for a component to edit atom
directly, let's separate the business logic from the component.
This can also be done easily with Recoil, but first, there are a few notes specific to Recoil.
atom can be used only in components
I mentioned that atom
is like a global variable, but in fact it is shared only among components enclosed by the <RecoilRoot>
component.
Moreover, atom
can only be used within the function component, and atom
cannot be referenced from outside the function component.
Therefore, to operate atom
outside of the component, define a generic logic in a function and call it from within the function component to apply the logic to atom
in the component.
The following is an example of Up/Down, which can be performed with arbitrary values by separating the logic from the previous one.
const addCountAtom: (a: CallbackInterface) => (a: number) => void = ({ snapshot, set }) => async (addValue: number) => { const current = await snapshot.getPromise(countAtom); set(countAtom, current + addValue); }; function UpDown() { const [count, setCount] = useRecoilState(countAtom); const addCount = useRecoilCallback(addCountAtom); return ( <div> <button onClick={() => addCount(-3)}>Down</button> {count} <button onClick={() => addCount(3)}>Up</button> </div> ); }
The addCountAtom()
is the logic part, which acquires the current atom
value and sets atom
to the value obtained by adding the number of arguments.
By passing this to useRecoilCallback()
, the function will be executed using the component atom
.
It may look very long, but it is actually only two lines, and the TypeScript type definition makes it look long.
selector
In addition to atom
, Recoil also has a value of selector
.
The selector
does not hold a value by itself, but returns a value obtained by calculation from another atom
.
When the original atom
is updated, the selector
value is also recalculated.
For example, let's create selector
that takes a value that multiplies the count by 100 and display it.
const countSelector = selector<number>({ key: `countSelector`, get: ({ get }) => get(countAtom) * 100, }); function ViewCount100() { const count = useRecoilValue(countSelector); return ( <div> {count} </div> ); } function App() { return ( <RecoilRoot> <UpDown /> <ViewCount100 /> <ViewCount100 /> <ViewCount100 /> </RecoilRoot> ); }
Since selector
has no value to change, use useRecoilValue()
, which does not get the change function, to get the value.
Non-simultaneous updates
Next, let's look at how to update values asynchronously.
asynchronous update of atom
The first is the update of atom
, which is the aforementioned business logic section.
However, as you can see from the use of async/await
, this is itself an asynchronous function, so you can write asynchronous processing with await
without any special consideration.
For example, the logic for a one-second delay update is shown below. It is exactly the same.
const addCountAtom: (a: CallbackInterface) => (a: number) => void = ({ snapshot, set }) => async (addValue: number) => { const current = await snapshot.getPromise(countAtom); await new Promise((resolve) => { setTimeout(() => resolve(), 1000); }); set(countAtom, current + addValue); };
Asynchronous update of selector
The other is to update selector
. If the get()
function in the selector
definition is changed to async
, asynchronous processing can be written for data acquisition.
For example, selector
, which is updated with a 1-second delay, is shown below. This is just as it should be.
const countSelector = selector<number>({ key: `countSelector`, get: async ({ get }) => { const count = get(countAtom); await new Promise((resolve) => { setTimeout(() => resolve(), 1000); }); return count * 100; } });
There is one caveat, however. await
to get the value before asynchronous processing, otherwise you will get stuck in an infinite loop.
waiting for updates
When a state is updated asynchronously, it is common to give the user some other display, such as a display of weights, until it is updated.
Recoil also supports this by default.
In the case of the asynchronous update of selector
described earlier, by enclosing the component that uses selector
in <Suspense>
, <Suspense>
will be displayed until the value of selector
is updated.
Then, when the value is confirmed, the original component will be displayed instead of <Suspense>
.
function App() { return ( <RecoilRoot> <UpDown /> <Suspense fallback={<div>Loading...</div>}> <ViewCount100 /> <ViewCount100 /> <ViewCount100 /> </Suspense> </RecoilRoot> ); }
However, this function is not as simple as "enclose it with '<Suspense>
'"; it is the maximum that can be done with the current version of React, and behind the scenes it is working according to the specifications for the "Concurrent Mode" that will be introduced in future versions of React.
What is Concurrent Mode?
Concurrent Mode is a new drawing mode that will be introduced in React in the future.
React rendering is a synchronous process in which all data to be displayed is determined before rendering.
The process of displaying weights during data acquisition required the user to implement his or her own way of knowing the state of the data in advance and deciding what to display at that point, including the display of weights, before drawing the data.
Concurrent Mode is a new React rendering mode that allows the data to have "confirmed," "retrieving," and "failed" states, so that it can be displayed in different ways depending on these states when rendering.
With this as a standard feature of React, weight processing, which used to be troublesome, can now be easily implemented by simply writing in accordance with React's manners.
In fact, Recoil's data is designed to work as it is in the coming Concurrent Mode era, with the data having the above conditions.
This is a brief introduction to Recoil. Recoil is simple, intuitive, easy to use, and exactly what we wanted.
I thought I saw Facebook in its true colors.
In the past, when React Hooks was released, I was expecting to see a new status management feature, but was disappointed to find out that it was just so that function components could have status.
At the time, I wondered why a function component at this timing when there was already a class component. But now, when I touched Recoil, I felt that React Hooks was created in order to realize Recoil.
I had wondered why Facebook had proposed Flux but not created a full-fledged state library, and now I think I know why.
It seems to me that the extension of conventional React has its limitations and cannot produce a satisfactory product, so they have abandoned their previous assets and even added new features to bring in Recoil.
And the scope of the reform is not limited to function components, but extends to Concurrent Mode, the foundation of React's rendering.
Certainly, with these in place, most of the processing envisioned in SPA could be implemented simply by using a set of functions, without relying on the user's own implementation.
Considering that the era of "HTML+JavaScript" is likely to continue for application development in the browser, I felt that I saw Facebook's seriousness in creating an easy-to-use SPA library county, even if it means destroying what has been built up so far, rather than providing an inexpensive library.
Changing React is something that only Facebook can do, so I'm glad to see that they are willing to go that far.
Impressions, etc.
I couldn't write it all down here, but Recoil, React Hooks, and Concurrent Mode have many other features that I would use to create an SPA site, and I thought they were well thought out from the perspective of site production.
Although it is still in the development stage, I think Recoil is comfortable enough to use with the current React.
Until now, when I wanted to create some small site, I really wanted to use React to create a quick site, but I was hesitant because of the amount of coding involved.
The code is much cleaner now that function components have become mainstream, and I would like to use Recoil from now on to create sites quickly and easily!