在 React 项目优雅地组合使用 Redux 和 React Query

873 阅读5分钟

在以往开发过的 React 项目中,经常会用到 Redux 和 React Query,似乎一提起 ”状态“ 和 ”数据“ 两个关键词就会立刻想到他们。Redux 很强大,能解决复杂问题,扩展性也不错,配上 Redux DevTools 调试成本变得很低。React Query 也很好,用法简单,功能全面,同时也提供了 DevTools。有时甚至有种错觉,有了他们做 React 开发就好像再也没有难题了,直到有这么两个场景频繁出现。

问题场景

场景 1 - 传播数据拉取结果

有两个 React 组件 A、B,组件 A 准备了一些参数拉取了一份数据做了渲染,组件 B 希望基于组件 A 拉取的这份数据做渲染。

场景 2 - 传播普通状态

有两个 React 组件 A、B,组件 A 维护了一个本地状态,组件 B 希望基于组件 A 的这个本地状态做渲染。

潜在方法

对于场景 1,如果数据拉取是用 React Query 做的,可以有 2 种方法解决。但不管是哪种方法,首先都需要封装一个基于 useQuery 的数据拉取 Hook。之后,第一种方法是将组件 A 中数据拉取 Hook 的返回值作为 Redux 状态实时更新,组件 B 访问这个 Redux 状态即可访问数据。第二种方法是将组件 A 中传入数据拉取 Hook 的参数作为 Redux 状态实时更新,组件 B 访问这个 Redux 状态获得相同参数调用数据拉取 Hook,借助 React Query 缓存机制访问同一份数据。

对于场景 2,要把组件 A 的本地状态升级成一个 Redux 状态进行维护,组件 B 则是访问这个 Redux 状态。

烦恼

上面两个场景只是想让组件 B 访问组件 A 的一块内存,但是实现起来却要先引入 Redux 状态,开发和维护的工作量直线上升。当然,不引入 Redux 状态也不是不可行,比如直接使用 React Context ,只是这样的话就得自己建立一套规则维护字段增减保持可扩展性,同时这也丢掉了 Redux DevTools 带来的调试便利,不是很划算。

那么,在 React 组件间传播状态和数据时,有没有一种方法既可以不引入 Redux 状态减少工作量又可以保留 Redux 生态带来的便利呢?同时这种方法应该兼容 React Query。

解决方法

其实通过一些封装把这些场景下 Redux 状态的开发和维护 “屏蔽” 掉就可以解决烦恼了,比如以下这样的一对 Hook 以及他们对应的非 Hook 版方法:

  • useSpreadOut<T>(index: unknown, value: T): T;,以 index 为索引将 value 映射成一个 Redux 状态。当 value 发成变化时,对应 Redux 状态也会跟着变化,并且及时触发所在 React 组件的重新渲染。当所在 React 组件销毁时,对应 Redux 状态也会跟着销毁。注意,即使 value 是 React Query 的 Hook 调用返回值,对应 Redux 状态也要能够跟着变化或销毁。Hook 的返回值则是直接把 value 透传出来。
  • useSpreadIn<T>(index: unknown): T | undefined;,以 index 为索引访问对应的 Redux 状态,当找不到索引的值时返回 undefined。当对应的 Redux 状态变化或销毁时能够及时触发所在 React 组件的重新渲染。
  • setSpreadOut<T>(index: unknown, value: T): T;,非 Hook 版的 useSpreadOut
  • getSpreadIn<T>(index: unknown): T | undefined;,非 Hook 版的 useSpreadIn

对于场景 1,组件 A 通过 Hook 组合 useSpreadOut(..., useQuery(...)) 在使用参数拉取数据的同时将数据拉取结果传播出去,组件 B 通过 useSpreadIn 访问被传播出来的数据。

对于场景 2,组件 A 通过 setSpreadOutuseSpreadIn 维护一个可以被传播出来的状态,组件 B 通过 useSpreadIn 访问被传播出来的状态。

因为不必再关心 Redux 状态的开发和维护,减少了许多的工作量,同时因为是基于 Redux 的封装,仍然可以继续借助 Redux DevTools 方便调试。

更进一步

由于这些封装比较实用,就进一步提炼成了一个独立 npm 包 Spreado。现在,可以通过 npm 命令直接安装使用:

$ npm i -S spreado

然后,在 React 项目顶级组件这么初始化一下就好:

// Requires peer dependencies installed: `react`, `redux`, `react-redux`, `react-query`.
import React, {FC} from 'react';
import {QueryClient, QueryClientProvider} from 'react-query';
import {Provider as ReduxProvider} from 'react-redux';
import {combineReducers, createStore} from 'redux';
import {SpreadoSetupProvider} from 'spreado';
import {
  spreadoReduxReducerPack,
  SpreadoSetupForReduxReactQuery,
} from 'spreado/for-redux-react-query';

const store = createStore(combineReducers(spreadoReduxReducerPack));
const queryClient = new QueryClient();
const spreadoSetup = new SpreadoSetupForReduxReactQuery({store, queryClient});

const App: FC = () => {
  return (
    <ReduxProvider store={store}>
      <QueryClientProvider client={queryClient}>
        <SpreadoSetupProvider setup={spreadoSetup}>
          <div>...</div>
        </SpreadoSetupProvider>
      </QueryClientProvider>
    </ReduxProvider>
  );
};

对于场景 1,解决起来是这个样子:

import {useQuery} from 'react-query';
import {useSpreadIn, useSpreadOut} from 'spreado';

const INDEX_OF_SOME_DATA_QUERY = 'INDEX_OF_SOME_DATA_QUERY';

function useSomeDataQuerySpreadOut(params: unknown) {
  return useSpreadOut(
    INDEX_OF_SOME_DATA_QUERY,
    useQuery([INDEX_OF_SOME_DATA_QUERY, params], () => /* 使用 params 拉取数据 */)
  );
}

function useSomeDataQuerySpreadIn() {
  return useSpreadIn<ReturnType<typeof useSomeDataQuerySpreadOut>>(INDEX_OF_SOME_DATA_QUERY, {});
}

const ComponentA: FC = () => {
  const params = /* 准备 params 用于拉取数据 */;
  const {isLoading, isSuccess, data, refetch} = useSomeDataQuerySpreadOut(params);
  return (
    <div>
      {isLoading && <Loading />}
      {isSuccess && <ResultA data={data} />}
      <button onClick={() => refetch()}>Refresh data</button>
    </div>
  );
};

const ComponentB: FC = () => {
  const {isLoading, isSuccess, data} = useSomeDataQuerySpreadIn();
  return (
    <div>
      {isLoading && <Loading />}
      {isSuccess && <ResultB data={data} />}
    </div>
  );
};

对于场景 2,解决起来是这个样子:

import {setSpreadOut, useSpreadIn} from 'spreado';

const INDEX_OF_IS_SOMETHING_VISIBLE = 'INDEX_OF_IS_SOMETHING_VISIBLE';

function useIsSomethingVisible() {
  return useSpreadIn<boolean>(INDEX_OF_IS_SOMETHING_VISIBLE, false);
}

function setIsSomethingVisible(v: boolean) {
  return setSpreadOut(INDEX_OF_IS_SOMETHING_VISIBLE, v);
}

const ComponentA: FC = () => {
  const isSomethingVisible = useIsSomethingVisible();
  return (
    <div>
      {isSomethingVisible && <div>Part A related to something</div>}
      <button onClick={() => setIsSomethingVisible(true)}>Show</button>
      <button onClick={() => setIsSomethingVisible(false)}>Hide</button>
      <div>Everything else in component A</div>
    </div>
  );
};

const ComponentB: FC = () => {
  const isSomethingVisible = useIsSomethingVisible();
  return (
    <div>
      {isSomethingVisible && <div>Part B related to something</div>}
      <div>Everything else in component B</div>
    </div>
  );
};

目前,Spreado 提供的封装可以支持运行在几个主流 状态管理库 和 数据拉取库 的交叉组合之上,包括:Redux/RTK + React Query、MobX + React Query、Redux/RTK + SWR、MobX + SWR,使用上的区别仅是在初始化的时候选择合适的 SpreadoSetup...,API 上则是保持一致的。

写在最后

以上,这两个令人烦恼的场景就得到了比较彻底的解决掉,通过将 传播问题 交给 Spreado,就可以保持低成本、优雅地组合使用 Redux 和 React Query。

如果有任何的疑问或想法,欢迎在评论区留言或在 spreado/issues 留言,如果觉得这种思路有帮助,可以给 GitHub repo react-easier/spreado 点个 ⭐️,比心。