Spreado - 轻松地在 React 组件间传播状态和数据

931 阅读6分钟

在 React 应用开发中,经常会一起使用 状态管理库数据拉取库,比如:Redux + React QueryMobX + SWR 或者其他组合。但是,即使有这些库的帮助,在 React 组件间共享 状态数据拉取结果 仍然是一件有点费力的事情。尽管状态管理库解决了组件间共享状态的问题,但是它们经常有自己特殊的用法约定和概念,仅是共享一个简单的布尔值都要写一定量的模板代码。而数据拉取库只关注数据问题,并不关心如何在组件间共享数据拉取结果,如果借助状态管理库共享数据拉取结果,那将又是相当一定的工作量。

把以上问题可以归纳为 React 组件间状态和数据的传播问题。针对这个问题,Spreado 提供了一系列易用的封装极大简化了解决过程。

基本理念

首先,Spreado 并非另一个新的状态管理库或数据拉取库,而是将状态管理库和数据拉取库视为对等依赖(peer dependency)进行的一层易用封装,作用是尽可能简化在 React 组件间传播状态和数据。

其次,Spreado 以非侵入的方式兼容当下流行的状态管理库和数据拉取库,让开发者能够继续使用原库的任何中间件和插件,同时保留了直接使用状态管理库或数据拉取库来解决极个别情况的可能性。

用法,以 Redux + React Query 为例

可以先借助 CRA 新建一个 React 示例应用,然后安装上自己常用的状态管理库和数据拉取库,我们以 Redux + React Query 为例:

$ npm i -S redux react-redux react-query

完成后把 Spreado 安装上:

$ npm i -S spreado

如果是 Yarn 爱好者,把 npm 换成 yarn 就好。

准备好后在应用的顶级组件这样初始化一下 Context:

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}>
          ...
        </SpreadoSetupProvider>
      </QueryClientProvider>
    </ReduxProvider>
  );
};

传播一个简单状态

假设有两个 React 组件 A、B 要共享一个布尔值,这个布尔值控制着他们各自部分元素的显示,同时其中一个组件能够控制这个布尔值。通过 Spreado,可以这么实现:

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>
  );
};

可以对比一下基于 Redux 的实现,发现因为不必再深入 Redux 特殊的用法约定和概念,省去了许多模板代码和理解成本:

import { produce } from 'immer';
import { useDispatch, useSelector } from 'react-redux';
import { Action, combineReducers, createStore } from 'redux';

interface GlobalState {
  isSomethingVisible: boolean;
}

const initialGlobalState: GlobalState = { isSomethingVisible: false };

interface GlobalAction extends Action, Partial<GlobalState> {}

const SET_IS_SOMETHING_VISIBLE = 'SET_IS_SOMETHING_VISIBLE';

function setIsSomethingVisible(v: boolean): GlobalAction {
  return { type: SET_IS_SOMETHING_VISIBLE, isSomethingVisible: v };
}

function globalReducer(state: GlobalState = initialGlobalState, action: GlobalAction): GlobalState {
  return produce(state, (draft) => {
    switch (action.type) {
      case SET_IS_SOMETHING_VISIBLE:
        draft.isSomethingVisible = action.isSomethingVisible ?? draft.isSomethingVisible;
        break;
    }
  });
}

const store = createStore(combineReducers({ global: globalReducer }));

type RootState = ReturnType<typeof store.getState>;

function useIsSomethingVisible() {
  return useSelector((rootState: RootState) => rootState.global.isSomethingVisible);
}

const ComponentA: FC = () => {
  const dispatch = useDispatch();
  const isSomethingVisible = useIsSomethingVisible();
  return (
    <div>
      {isSomethingVisible && <div>Part A related to something</div>}
      <button onClick={() => dispatch(setIsSomethingVisible(true))}>Show</button>
      <button onClick={() => dispatch(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>
  );
};

传播一个数据拉取结果

假设有两个 React 组件 A、B 要共享一个 React Query 的数据拉取结果,这个数据拉取结果决定着他们各自不同的渲染行为,同时其中一个组件能够控制刷新。通过 Spreado,可以这么实现:

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

const INDEX_OF_SOME_DATA_QUERY = 'INDEX_OF_SOME_DATA_QUERY';

function useSomeDataQuerySpreadOut(params: Record<string, string>) {
  return useSpreadOut(
    INDEX_OF_SOME_DATA_QUERY,
    useQuery([INDEX_OF_SOME_DATA_QUERY, params], () => /* 使用传入的参数请求数据 */)
  );
}

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

const ComponentA: FC = () => {
  const 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>
  );
};

对比 Redux 实现而言,仍是省去了许多模板代码和理解成本。对比 React Query 封装复用来看,现在只需在其中一个组件准备请求数据的参数即可。尽管请求参数的准备也可以封装复用,但是当其中的一些值涉及到表现层逻辑时,比如取值自表单的某个字段,封装复用这个过程也会变得十分艰难。而使用了 useSpreadIn 就轻松省去了这些麻烦。

支持更多组合

目前 Spreado 已经支持了主流的状态管理库和数据拉取库,即 ReduxRedux ToolkitMobXReact QuerySWR 的任意交叉组合。相比 Redux + React Query,基于不同的组合使用 Spreado 区别只在于 Context 的初始化过程略有不同,其他用法仍保持完全一致。比如 Redux Tool + SWR 的初始化过程如下:

import {configureStore} from '@reduxjs/toolkit';
import React, {FC} from 'react';
import {Provider as ReduxProvider} from 'react-redux';
import {SpreadoSetupProvider} from 'spreado';
import {spreadoReduxReducerPack, SpreadoSetupForReduxSwr} from 'spreado/for-redux-swr';

const store = configureStore({
  reducer: spreadoReduxReducerPack,
  middleware: (m) => m({serializableCheck: false}),
});
const spreadoSetup = new SpreadoSetupForReduxSwr({store});

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

关于基于不同组合的更多使用,可以参考官方文档 Initialization 章节

核心 API 和服务端渲染(SSR)

Spreado 的核心 API 只有 4 个,useSpreadOutuseSpreadInsetSpreadOutgetSpreadInuse 开头的是 React Hook 方法,get/set 开头的是普通方法,Out 后缀的用于从当前 React 组件送出传播对象,In 后缀的用于在当 React 前组件接收传播对象。更多详情可以参考官方文档 API 章节,方法结构摘录如下:

useSpreadOut<T>(index: unknown, value: T): T;

useSpreadIn<T>(index: unknown): T | undefined;
useSpreadIn<T>(index: unknown, fallback: Partial<T>): T | Partial<T>;
useSpreadIn<T>(index: unknown, fallback?: Partial<T>): T | Partial<T> | undefined;

setSpreadOut<T>(index: unknown, value: T): T;
setSpreadOut<T>(index: unknown, callback: (value?: T) => T): T;

getSpreadIn<T>(index: unknown): T | undefined;
getSpreadIn<T>(index: unknown, fallback: Partial<T>): T | Partial<T>;
getSpreadIn<T>(index: unknown, fallback?: Partial<T>): T | Partial<T> | undefined;

此外,Spreado 已经全面支持服务端渲染(Server Side Rendering)并且简化、统一了部分处理细节,具体用法可以参考官方文档 SSR 章节

FAQ

经常有 React Query 的使用者会问在 React 组件间传播数据时难道不可以直接用 queryClient.getQueryData 吗?结论是不行。原因是 queryClient.getQueryData 并非 React Hook 方法,不能察觉数据拉取结果的随时变化从而及时地触发所在 React 组件的重新渲染,而 useSpreadOut/In 就解决好了这方面的局限。

写在最后

在组合使用 状态管理库数据拉取库 时会藏有许多这样那样的小问题,而 Spreado 旨在将 React 组件间状态和数据的传播问题一网打尽,让我们可以更加专注于 React 应用逻辑。如果有任何的疑问或想法,欢迎在 spreado/issues 留言,中英文都可以。如果有时间和兴趣,欢迎直接提交代码,具体可以参考开发引导。如果觉得小工具有帮助,请给 GitHub repo react-easier/spreado 点个 ⭐️,这也是我们不断前行的动力 😃。