如何使用Recoil在React组件外部操作全局状态?

3
我正在使用Recoil,并希望从实用函数内部访问存储(获取/设置),与组件外部进行交互。
更一般地说,人们如何编写可重复使用的函数来使用Recoil操作全局状态?使用Redux,我们可以直接向存储分发事件,但我还没有在Recoil中找到替代方案。
使用hooks是一种很好的开发者体验,但难以将在组件内定义的函数转换为外部实用函数,因为hooks只能在组件内部使用。

Recoil 没有“全局”状态。您可以在 Recoil 组件图下方找到原子。将 Recoil 与 React 相关联的唯一事物是 RecoilRoot 组件。 但是,也许您可以提供一些伪代码,以便我更好地理解问题和您想要做什么。 - Johannes Klauß
没错,即使只使用一个 RecoilRoot,人们可能会认为它是全局存储。 - Vadorequest
3个回答

7
您可以使用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);
}


谢谢!我还没有尝试过,但有一个适当的(并且得到维护)替代方案很棒。 - Vadorequest

4

我成功地改编了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)
}

你能再解释一下使用方法吗? - Pranta
@Pranta,你可以在非React元素(函数等)中使用setRecoilExternalState(lastCreatedUserState, newUser)。这样清楚了吗?这是我编写的应用程序的一个实际示例:https://github.com/Vadorequest/rwa-faunadb-reaflow-nextjs-magic/search?q=setRecoilExternalState%28 - Vadorequest
嘿,getServersideprops似乎无法正常工作。 - Pranta
我不会期望在getServerSideProps中起作用,因为RecoilRoot组件尚未定义。此时您的Recoil状态甚至不存在,对吗?也许如果您在_app.js中定义<RecoilRoot>并从页面调用getServerSideProps,则可能会起作用,但我不确定那是否有效。 - Vadorequest

0
一些简单的技巧,没有任何npm包和复杂的东西,但我不确定这是否是一个好的方法 :) 但它非常有效。
在某些高阶组件(HOC)中定义ref和imperativeHandle。
1. 组件外部(在组件声明的顶部)
// typescript version
export const errorGlobalRef = createRef<{
  setErrorObject: (errorObject: ErrorTypes) => void;
}>();

// javascript version
export const errorGlobalRef = createRef();
  1. 组件内部
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);
  }
);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接