在以往开发过的 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 通过 setSpreadOut、useSpreadIn 维护一个可以被传播出来的状态,组件 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 点个 ⭐️,比心。