'How to update atoms (state) in Recoil.js outside components ? (React)
I'm new to Recoil.js, I have the following atom and selector for the signed-in user in the app:
const signedInUserAtom = atom<SignedInUser | null>({
key: 'signedInUserAtom',
default: null
})
export const signedInUserSelector = selector<SignedInUser | null>({
key: 'signedInUserSelector',
get: ({ get }) => get(signedInUserAtom),
set: ({ set, get }, newUserValue) => {
// ... Do a bunch of stuff when a user signs in ...
set(signedInUserAtom, newUserValue)
}
})
So basically I use signedInUserSelector
in order to set the new user.
Now, I want to have a few functions that will set the user through the selector, and use them across my components, like:
export async function signInWithGoogleAccount() {
const googleUser = async googleClient.signIn()
// here I need to set the user atom like:
// const [user, setUser] = useRecoilState(signedInUserSelector)
// setUser(googleUser)
}
export async function signInWithLocalAccount(email: string, password: string) {
const localUser = async localClient.signIn(email, password)
// here I need to set the user atom like:
// const [user, setUser] = useRecoilState(signedInUserSelector)
// setUser(localUser)
}
export async function signOut() {
await localClient.signOut()
// here I need to set the user atom like:
// const [user, setUser] = useRecoilState(signedInUserSelector)
// setUser(null)
}
The problem is since these functions are not defined inside components I can't use recoil hooks (like useRecoilState
to access selectors/atoms).
In the end I want to have any component to be able to do:
function SignInFormComponent() {
return <button onClick={signInWithGoogleAccount}>Sign In</button>
}
But how can I access selectors/atoms in signInWithGoogleAccount
if it is not in a component?
Solution 1:[1]
I think the only way (at least as of a few months ago) is a sort of hack where you include a non-rendering component that uses the recoil hooks and exports the provided functions from them.
See: https://github.com/facebookexperimental/Recoil/issues/289#issuecomment-777249693
Below is the file from my own project that achieves this, heavily based on that link above. All you need to do is put <RecoilExternalStatePortal />
anywhere in your application tree that is guaranteed to always render.
This seems like an omission in the Recoil API, IMHO.
import React from 'react'
import { Loadable, RecoilState, RecoilValue, useRecoilCallback, useRecoilTransactionObserver_UNSTABLE } from 'recoil'
/**
* Returns a Recoil state value, from anywhere in the app.
*
* Can be used outside of the React tree (outside a React component), such as in utility scripts, etc.
* <RecoilExternalStatePortal> must have been previously loaded in the React tree, or it won't work.
* Initialized as a dummy function "() => null", it's reference is updated to a proper Recoil state mutator when RecoilExternalStatePortal is loaded.
*
* @example const lastCreatedUser = getRecoilExternal(lastCreatedUserState);
*/
export function getRecoilState<T>(recoilValue: RecoilValue<T>): T {
return getRecoilLoadable(recoilValue).getValue()
}
/** The `getLoadable` function from recoil. This shouldn't be used directly. */
let getRecoilLoadable: <T>(recoilValue: RecoilValue<T>) => Loadable<T> = () => null as any
/**
* Sets a Recoil state value, from anywhere in the app.
*
* Can be used outside of the React tree (outside a React component), such as in utility scripts, etc.
*
* <RecoilExternalStatePortal> must have been previously loaded in the React tree, or it won't work.
* Initialized as a dummy function "() => null", it's reference is updated to a proper Recoil state mutator when RecoilExternalStatePortal is loaded.
*
* @example setRecoilExternalState(lastCreatedUserState, newUser)
*/
export let setRecoilState: <T>(recoilState: RecoilState<T>, valOrUpdater: ((currVal: T) => T) | T) => void = () =>
null as any
/**
* Utility component allowing to use the Recoil state outside of a React component.
*
* It must be loaded in the _app file, inside the <RecoilRoot> component.
* Once it's been loaded in the React tree, it allows using setRecoilExternalState and getRecoilExternalLoadable from anywhere in the app.
*
* @see https://github.com/facebookexperimental/Recoil/issues/289#issuecomment-777300212
* @see https://github.com/facebookexperimental/Recoil/issues/289#issuecomment-777305884
* @see https://recoiljs.org/docs/api-reference/core/Loadable/
*/
export function RecoilExternalStatePortal() {
// We need to update the getRecoilExternalLoadable every time there's a new snapshot
// Otherwise we will load old values from when the component was mounted
useRecoilTransactionObserver_UNSTABLE(({ snapshot }) => {
getRecoilLoadable = snapshot.getLoadable
})
// We only need to assign setRecoilExternalState once because it's not temporally dependent like "get" is
useRecoilCallback(({ set }) => {
setRecoilState = set
return async () => {
// no-op
}
})()
return <></>
}
Solution 2:[2]
As I pointed out in another answer, you generally don't want to run into this, but if you eventually really need to update atoms outside of React Components you might give a try to Recoil Nexus.
In the same file where you have your RecoilRoot you'll have something like:
import React from 'react';
import { RecoilRoot } from "recoil"
import RecoilNexus from 'recoil-nexus'
export default function App() {
return (
<RecoilRoot>
<RecoilNexus/>
{/* ... */}
</RecoilRoot>
);
};
export default App;
Then, wherever you need to read/update the values:
import yourAtom from './yourAtom'
import { getRecoil, setRecoil } from 'recoil-nexus'
Eventually you can get and set the values like this:
const loading = getRecoil(loadingState)
setRecoil(loadingState, !loading)
That's it!
Disclaimer: I am the author of the library.
Check this CodeSandbox for a live example.
Solution 3:[3]
I think the right way to handle this in react is with a custom hook/facade. That way you can keep code centralized, but share it with the components that need it, and in this case, include code that relies on hooks/being in a component. This article explains it fairly well:
https://wanago.io/2019/12/09/javascript-design-patterns-facade-react-hooks/
But the basic idea is that you would create an custom useAuth
hook, that would expose what you need:
export function useAuth() {
const [auth, setAuth] = useRecoilState(authAtom);
const resetAuth = useResetRecoilState(authAtom);
const authState = useMemo(() => {
return {
isAuthenticated: () => {
return !!auth.accessToken && auth.expiresAt > new Date();
},
};
}, [auth]);
...
return { auth, authState, logout: resetAuth, /* and maybe more, error, login, etc */ };
and then use it in components that need auth:
const AuthorizedComponent: FC<Props> = (props) => {
const { auth, authState } = useAuth();
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Alex Wayne |
Solution 2 | Luis Antonio Canettoli Ordoñez |
Solution 3 | chrismarx |