How to update atoms (state) in Recoil.js outside components ? (React)
Asked Answered
E

3

17

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?

Elide answered 26/8, 2021 at 21:35 Comment(0)
M
29

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:

// Read value
const loading = getRecoil(loadingState)
console.table({ loading })

// Set value
setRecoil(loadingState, true)

Or, if the new state depends on the previous one, it's recommended to use an updater function to correctly batch React state updates:

setRecoil(loadingState, loading => !loading)

That's it!

Disclaimer: I am the author of the library.


Check this CodeSandbox for a live example.

Mild answered 28/9, 2021 at 12:13 Comment(6)
dude thanks awesome, u save my daySaltwater
This should be the accepted answer, and the library should IMHO be integrated into Recoil itself. While i do agree with the premise here, not everything in an app necessarily happens inside or deeply integrated with the React tree, so having access from outside seems like a must have. Well done Luis, keep up the good work!Ite
this is great, by far the only solution I came across while trying to open a modal from an axios interceptor when catching a specific error.Raspings
when invoke setRecoil() to a selector, an error arised: Setting selectors within atomicUpdate is not supported.Mylonite
@AlexKon Since requested by many, v0.5.0 is now supporting selectorsMoskva
Hi everyone, i got this error when get state from atom: TypeError: nexus.get is not a function at getRecoil (recoil v0.7.7, recoil-nexus v0.5.0), any suggestion?Consubstantial
S
5

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 <></>
}
Styliform answered 26/8, 2021 at 23:23 Comment(0)
L
1

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();
Lornalorne answered 9/5, 2022 at 16:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.