乐闻世界logo
搜索文章和话题

React Recoil 如何在组件外部更新的 atom 原子状态?

7 个月前提问
5 个月前修改
浏览次数109

4个答案

1
2
3
4

在 React Recoil 中,通常我们会在组件内部使用 Recoil 的 useRecoilStateuseSetRecoilState 钩子来更新 atom 原子状态。但是,在某些场景下,我们可能需要在组件外部,例如在一个异步函数或者一个普通的 JavaScript 模块中更新 Recoil 的状态。为了实现这一点,我们可以使用 Recoil 的 RecoilRootatom API 来创建全局状态,并使用 useRecoilCallback 钩子来创建一个可以在组件外部调用的更新函数。

下面是在组件外部更新 atom 状态的步骤:

  1. 定义一个 atom:
jsx
import { atom } from 'recoil'; export const myAtom = atom({ key: 'myAtom', default: 0, // 初始值 });
  1. 在组件树中提供一个 RecoilRoot:
jsx
import React from 'react'; import { RecoilRoot } from 'recoil'; import App from './App'; function RootComponent() { return ( <RecoilRoot> <App /> </RecoilRoot> ); }
  1. 使用 useRecoilCallback 创建可以从组件外部调用的回调函数:
jsx
import { useRecoilCallback } from 'recoil'; import { myAtom } from './store'; function useUpdateAtom() { const updateAtom = useRecoilCallback(({ set }) => (newValue) => { set(myAtom, newValue); }, []); return updateAtom; }
  1. 在组件内部调用 useUpdateAtom 并将返回的函数暴露给外部:
jsx
import React from 'react'; import { useUpdateAtom } from './useUpdateAtom'; function MyComponent() { const updateAtomFromOutside = useUpdateAtom(); // 你现在可以将 updateAtomFromOutside 函数传递给外部或者注册为全局方法 // 例如绑定到 window 对象,或者传递给需要调用更新操作的外部模块 return ( <div> <button onClick={() => updateAtomFromOutside(10)}>Update Atom</button> </div> ); }
  1. 在组件外部使用该函数更新 atom 状态:
javascript
// 假设你已经在合适的地方获取到了 updateAtomFromOutside 函数 updateAtomFromOutside(20); // 这将会更新 myAtom 的值为 20

通过这种方式,我们可以轻松地在组件外部更新 Recoil 的状态,同时还能保持与 Recoil 状态管理库的整体架构兼容。这对于处理那些不直接绑定在 React 组件生命周期上的逻辑,如定时器、网络请求回调等情况特别有用。

2024年6月29日 12:07 回复

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:

shell
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:

shell
import yourAtom from './yourAtom' import { getRecoil, setRecoil } from 'recoil-nexus'

Eventually you can get and set the values like this:

shell
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.

2024年6月29日 12:07 回复

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.

shell
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 <></> }
2024年6月29日 12:07 回复

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:

shell
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:

shell
const AuthorizedComponent: FC<Props> = (props) => { const { auth, authState } = useAuth();
2024年6月29日 12:07 回复

你的答案