Installing recoil
npm install recoil
Add RecoilRoot to your root component
import React from 'react'; import {RecoilRoot} from 'recoil'; function App() { return ( <RecoilRoot> <YourComponent /> </RecoilRoot> ); }
Atoms
An atom
- is (a piece) of state that can shared between your components
- is what components can use to read from or write to
- will trigger a re-render for any component (that uses the atom) whenever the atom state changed
You create atom shared state like that:
const fontSizeState = atom({ key: 'fontSizeState', // key needs to be globally unique default: 14, });
Recoil provides several hooks to interact with atoms:
- useRecoilValue(myAtom) will read the state
- useSetRecoilState(myAtom) will set new state
- useRecoilState(myAtom) will return two things: the atom’s state and a state setter function
Use useRecoilState to “subscribe” your Component and to read from or write to an atom:
// Component A function FontButton() { const [fontSize, setFontSize] = useRecoilState(fontSizeState); /* if you only need to set value const setFontsize = useSetRecoilState(fontSizeState); setFontsize(18) // or alternatively to obtain old value setFontsize((oldFontsize) => oldFontsize + 2); */ // if you only need to get value // const fontSize = useRecoilValue(fontSizeState); return ( <button onClick={() => setFontSize((size) => size + 1)} style={{fontSize}}> Click to Enlarge </button> ); }
// Component B will receive the new value when button was clicked in Component A function Text() { const [fontSize, setFontSize] = useRecoilState(fontSizeState); return <p style={{fontSize}}>This text will increase in size too.</p>; }
Selectors
Selectors are pure functions that you use to read state which was derived/transformed either sync or async from other state, for example filtered lists or the total numbers of objects that some state contains.
When creating selectors keep in mind that they represent idempotent/pure functions: For a given set of inputs they should always produce the same results, because they may be cached, restarted, or executed multiple times.
Selectors are used like selector(input)
where input
is either an atom or another selector. When this input is updated, the selector function will be re-evaluated too.
This is how you create a readable selector:
const fontSizeLabelState = selector({ key: 'fontSizeLabelState', get: ({get}) => { const fontSize = get(fontSizeState); // here we get state from the atom const unit = 'px'; return `${fontSize}${unit}`; }, });
Additionally to the selectors’ get
you can define a set
for the selector. This is how you create a writable selector:
const proxySelector = selector({ key: 'ProxySelector', get: ({get}) => ({...get(myAtom), extraField: 'hi'}), set: ({set}, newValue) => set(myAtom, newValue), });
Finally, components can “subscribe” to selectors to read or – if the selector has a set
method – to set state:
function FontButton() { const [fontSize, setFontSize] = useRecoilState(fontSizeState); const fontSizeLabel = useRecoilValue(fontSizeLabelState); return ( <> <div>Current font size: {fontSizeLabel}</div> <button onClick={() => setFontSize(fontSize + 1)} style={{fontSize}}> Click to Enlarge </button> </> ); }
A common pattern is to use an atom to represent local editable state, but use a selector to query default values:
const currentUserIDState = atom({ key: 'CurrentUserID', default: selector({ key: 'CurrentUserID/Default', get: () => myFetchCurrentUserID(), }), });
Async data
Create selector using async/await:
const currentUserNameQuery = selector({ key: 'CurrentUserName', get: async ({get}) => { const response = await myDBQuery({ userID: get(currentUserIDState), }); return response.name; }, });
Then use the data
function CurrentUserInfo() { const userName = useRecoilValue(currentUserNameQuery); return <div>{userName}</div>; }
But one more important thing: Since React render functions are synchronous, what will it render before the promise resolves? There are two options
Option 1: You wrap your component in a Suspense
boundary which will catch any descendants that are still pending and render a fallback UI
function MyApp() { return ( <RecoilRoot> <React.Suspense fallback={<div>Loading...</div>}> <CurrentUserInfo /> </React.Suspense> </RecoilRoot> ); }
Option 2: You use useRecoilValueLoadable
function UserInfo({userID}) { const userNameLoadable = useRecoilValueLoadable(userNameQuery(userID)); switch (userNameLoadable.state) { case 'hasValue': return <div>{userNameLoadable.contents}</div>; case 'loading': return <div>Loading...</div>; case 'hasError': throw userNameLoadable.contents; } }
Query based on parameters
Use selectorFamily
if you need to make an async request that uses a custom parameter (here userID
):
const userNameQuery = selectorFamily({ key: 'UserName', get: userID => async () => { const response = await myDBQuery({userID}); if (response.error) { throw response.error; } return response.name; }, });
Concurrent requests
Let’s assume you have an array containing friend objects and you want to retrieve all friends of that list by their id. To make these requests concurrently you use waitForAll
:
const friendsInfoQuery = selector({ key: 'FriendsInfoQuery', get: ({get}) => { const {friendList} = get(currentUserInfoQuery); const friends = get(waitForAll( friendList.map(friendID => userInfoQuery(friendID)) )); return friends; }, });
or waitForNone
:
const friendsInfoQuery = selector({ key: 'FriendsInfoQuery', get: ({get}) => { const {friendList} = get(currentUserInfoQuery); const friendLoadables = get(waitForNone( friendList.map(friendID => userInfoQuery(friendID)) )); return friendLoadables .filter(({state}) => state === 'hasValue') .map(({contents}) => contents); }, });
Pre-fetch data
For performance reasons you may wish to kick off fetching before rendering. You achieve this via useRecoilCallback
:
function CurrentUserInfo() { const currentUser = useRecoilValue(currentUserInfoQuery); const friends = useRecoilValue(friendsInfoQuery); const changeUser = useRecoilCallback(({snapshot, set}) => userID => { snapshot.getLoadable(userInfoQuery(userID)); // pre-fetch user info set(currentUserIDState, userID); // change current user to start new render }); return ( <div> <h1>{currentUser.name}</h1> <ul> {friends.map(friend => <li key={friend.id} onClick={() => changeUser(friend.id)}> {friend.name} </li> )} </ul> </div> ); }
AtomFamily
An Atom Family represents a collection of atoms. When you call atomFamily
it will return a function which provides the RecoilState
atom based on the parameters you pass in.
const elementPositionStateFamily = atomFamily({ key: 'ElementPosition', default: [0, 0], }); function ElementListItem({elementID}) { const position = useRecoilValue(elementPositionStateFamily(elementID)); return ( <div> Element: {elementID} Position: {position} </div> ); }
atomFamily allows you to use params for default values:
const myAtomFamily = atomFamily({ key: ‘MyAtom’, default: param => defaultBasedOnParam(param), });
Post data with custom React hook
In this example we create a custom React hook useRegister(username)
that will post a username to an API. We set the response via useSetRecoilState
within the hook which allows a component to set the value without subscribing the component to re-render when the value changes.
const useRegister = () => { const setAccount = useSetRecoilState(accountState); const register = useCallback(async (username)=> { const response = await fetch(url, { method: 'POST', body: { username }, }); const responseJson = await response.json(); setAccount(responseJson); }, [setAccount]); return register; } const RegisterForm = () => { const register = useRegister(); const handleSubmit = async (e) => { e.preventDefault(); await register(username); }; return ( <form onSubmit={handleSubmit}> <input type="text" name="username" /> <button type="submit">Register</button> </form> ); }