如何使用 Recoil 在 React 组件之外操作全局状态?
How to manipulate a global state outside of a React component using Recoil?
我正在使用 Recoil,我想从实用程序函数内访问组件 (get/set) 外的商店。
更一般地说,人们如何编写可重用函数来使用 Recoil 操纵全局状态?使用 Redux,我们可以直接将事件派发到商店,但我还没有找到 Recoil 的替代方案。
使用钩子是一种很棒的开发人员体验,但是很难将组件内定义的函数转换为外部实用程序函数,因为钩子只能在组件内使用。
我设法调整 https://github.com/facebookexperimental/Recoil/issues/289#issuecomment-777249693 答案并使其与 Next.js 框架一起工作。 (见下面的用法示例)
此解决方法允许将 Recoil Root 用作一种全局状态。不过,它只有在 只有一个 RecoilRoot
组件时才有效。
// RecoilExternalStatePortal.tsx
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 = getRecoilExternalLoadable(lastCreatedUserState);
*/
export let getRecoilExternalLoadable: <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 setRecoilExternalState: <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 }) => {
getRecoilExternalLoadable = snapshot.getLoadable;
});
// We only need to assign setRecoilExternalState once because it's not temporally dependent like "get" is
useRecoilCallback(({ set }) => {
setRecoilExternalState = set;
return async () => {
};
})();
return <></>;
}
使用Next.js框架的配置示例:
// pages/_app.tsx
import {
NextComponentType,
NextPageContext,
} from 'next';
import { Router } from 'next/router';
import React from 'react';
import { RecoilRoot } from 'recoil';
import { RecoilExternalStatePortal } from '../components/RecoilExternalStatePortal';
type Props = {
Component: NextComponentType<NextPageContext>; // Page component, not provided if pageProps.statusCode is 3xx or 4xx
err?: Error; // Only defined if there was an error
pageProps: any; // Props forwarded to the Page component
router?: Router; // Next.js router state
};
/**
* This file is the entry point for all pages, it initialize all pages.
*
* It can be executed server side or browser side.
*
* @see https://nextjs.org/docs/advanced-features/custom-app Custom _app
* @see https://nextjs.org/docs/basic-features/typescript#custom-app TypeScript for _app
*/
const App: React.FunctionComponent<Props> = (props): JSX.Element => {
const { Component, pageProps} = props;
return (
<RecoilRoot>
<Component {...pageProps} />
<RecoilExternalStatePortal />
</RecoilRoot>
);
};
// Anywhere, e.g: src/utils/user.ts
const createUser = (newUser) => {
setRecoilExternalState(lastCreatedUserState, newUser)
}
您可以使用 recoil-nexus,这是一个小包,其代码类似于 Vadorequest 的答案。
https://www.npmjs.com/package/recoil-nexus
// Loading example
import { loadingState } from "../atoms/loadingState";
import { getRecoil, setRecoil } from "recoil-nexus";
export default function toggleLoading() {
const loading = getRecoil(loadingState);
setRecoil(loadingState, !loading);
}
一些没有任何 npm 包和复杂东西的小 hack,但我不确定这样做是否是个好方法 :) 但效果很好。
在某些 HOC(高阶组件)中定义 ref 和 imperativeHanle
- 在组件外部(在组件声明之上)
// typescript version
export const errorGlobalRef = createRef<{
setErrorObject: (errorObject: ErrorTypes) => void;
}>();
// javascript version
export const errorGlobalRef = createRef();
- 内部组件
const [errorObject, setErrorObject] = useRecoilState(errorAtom);
//typescript version
useImperativeHandle(errorGlobalRef, () => {
return {
setErrorObject: (errorObject: ErrorTypes) => setErrorObject(errorObject),
};
});
//javascritp version
useImperativeHandle(errorGlobalRef, () => {
return {
setErrorObject: (errorObject) => setErrorObject(errorObject),
};
});
并在您需要的地方导入和使用 ;)
就我而言:
//axios.config.ts
instance.interceptors.response.use(
(response) => {
return response;
},
async function (error) {
const originalRequest = error.config;
if (error?.response?.data) {
const { data } = error.response;
if (data) {
// set recoil state
errorGlobalRef.current?.setErrorObject({
error: data.error,
message: typeof data.message === 'string' ? [data.message] : data.message,
statusCode: data.statusCode,
});
}
}
return Promise.reject(error);
}
);
我正在使用 Recoil,我想从实用程序函数内访问组件 (get/set) 外的商店。
更一般地说,人们如何编写可重用函数来使用 Recoil 操纵全局状态?使用 Redux,我们可以直接将事件派发到商店,但我还没有找到 Recoil 的替代方案。
使用钩子是一种很棒的开发人员体验,但是很难将组件内定义的函数转换为外部实用程序函数,因为钩子只能在组件内使用。
我设法调整 https://github.com/facebookexperimental/Recoil/issues/289#issuecomment-777249693 答案并使其与 Next.js 框架一起工作。 (见下面的用法示例)
此解决方法允许将 Recoil Root 用作一种全局状态。不过,它只有在 只有一个 RecoilRoot
组件时才有效。
// RecoilExternalStatePortal.tsx
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 = getRecoilExternalLoadable(lastCreatedUserState);
*/
export let getRecoilExternalLoadable: <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 setRecoilExternalState: <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 }) => {
getRecoilExternalLoadable = snapshot.getLoadable;
});
// We only need to assign setRecoilExternalState once because it's not temporally dependent like "get" is
useRecoilCallback(({ set }) => {
setRecoilExternalState = set;
return async () => {
};
})();
return <></>;
}
使用Next.js框架的配置示例:
// pages/_app.tsx
import {
NextComponentType,
NextPageContext,
} from 'next';
import { Router } from 'next/router';
import React from 'react';
import { RecoilRoot } from 'recoil';
import { RecoilExternalStatePortal } from '../components/RecoilExternalStatePortal';
type Props = {
Component: NextComponentType<NextPageContext>; // Page component, not provided if pageProps.statusCode is 3xx or 4xx
err?: Error; // Only defined if there was an error
pageProps: any; // Props forwarded to the Page component
router?: Router; // Next.js router state
};
/**
* This file is the entry point for all pages, it initialize all pages.
*
* It can be executed server side or browser side.
*
* @see https://nextjs.org/docs/advanced-features/custom-app Custom _app
* @see https://nextjs.org/docs/basic-features/typescript#custom-app TypeScript for _app
*/
const App: React.FunctionComponent<Props> = (props): JSX.Element => {
const { Component, pageProps} = props;
return (
<RecoilRoot>
<Component {...pageProps} />
<RecoilExternalStatePortal />
</RecoilRoot>
);
};
// Anywhere, e.g: src/utils/user.ts
const createUser = (newUser) => {
setRecoilExternalState(lastCreatedUserState, newUser)
}
您可以使用 recoil-nexus,这是一个小包,其代码类似于 Vadorequest 的答案。
https://www.npmjs.com/package/recoil-nexus
// Loading example
import { loadingState } from "../atoms/loadingState";
import { getRecoil, setRecoil } from "recoil-nexus";
export default function toggleLoading() {
const loading = getRecoil(loadingState);
setRecoil(loadingState, !loading);
}
一些没有任何 npm 包和复杂东西的小 hack,但我不确定这样做是否是个好方法 :) 但效果很好。
在某些 HOC(高阶组件)中定义 ref 和 imperativeHanle
- 在组件外部(在组件声明之上)
// typescript version
export const errorGlobalRef = createRef<{
setErrorObject: (errorObject: ErrorTypes) => void;
}>();
// javascript version
export const errorGlobalRef = createRef();
- 内部组件
const [errorObject, setErrorObject] = useRecoilState(errorAtom);
//typescript version
useImperativeHandle(errorGlobalRef, () => {
return {
setErrorObject: (errorObject: ErrorTypes) => setErrorObject(errorObject),
};
});
//javascritp version
useImperativeHandle(errorGlobalRef, () => {
return {
setErrorObject: (errorObject) => setErrorObject(errorObject),
};
});
并在您需要的地方导入和使用 ;) 就我而言:
//axios.config.ts
instance.interceptors.response.use(
(response) => {
return response;
},
async function (error) {
const originalRequest = error.config;
if (error?.response?.data) {
const { data } = error.response;
if (data) {
// set recoil state
errorGlobalRef.current?.setErrorObject({
error: data.error,
message: typeof data.message === 'string' ? [data.message] : data.message,
statusCode: data.statusCode,
});
}
}
return Promise.reject(error);
}
);