Spreado - 更简单的代码目录结构

241 阅读4分钟

前言

阅读本文之前,请先阅读 Spreado - 轻松地在 React 组件间传播状态和数据 ,确保你对 Spreado 已经有基本的了解。

作为软件开发工程师,我们都知道一个更清晰更简洁的代码目录结构,会让我们的日常开发更为便捷。没人喜欢一眼过去十几个目录不知所谓的杂乱感。

通过这一篇文章(Spreado - 轻松地在 React 组件间传播状态和数据)我们已经知道 Spreado 并没有引入任何额外的概念,它只是对 状态管理库数据拉取库 进行了一层易用封装,在这篇文章里,我们会简单的谈一谈,基于这层封装,我们项目中的目录结构将会得到怎样的简化。

以前的目录结构

以 Redux 和 React-Query 为例。

假如我们在一个 React 项目中选择使用 Redux 来管理状态,选择使用 React-Query 来从后台拉取数据时,我们首先需要梳理好如下概念:

  1. Redux 的 store、reducer、action.
  2. React-Query 的 useQuery 以及 useMutation 等 hooks.

如果我们想要实现这样一个常见业务:从后台拉取一个数据 someData,并在多个的 React 组件内共享使用,我们的代码结构大概是这样:

定义 action:

// src/store/actions.ts
import {Action} from 'redux';
import {GlobalState, SomeDataType} from './index';

export interface GlobalAction extends Action, Partial<GlobalState> {}

export const SET_SOME_DATA = 'SET_SOME_DATA';

export function setSomeData(v?: SomeDataType): GlobalAction {
  return {type: SET_SOME_DATA, someData: v};
}

定义 reducer:

// src/store/reducer.ts
import {produce} from 'immer';
import {GlobalState} from './index';
import {GlobalAction, SET_SOME_DATA} from './action';

const initialGlobalState: GlobalState = {someData: {}};

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

创建 store:

// src/store/index.ts
import {combineReducers, createStore} from 'redux';
import {globalReducer} from './reducer';

export type SomeDataType = object;

export interface GlobalState {
  someData?: SomeDataType;
}

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

使用 react-query 拉取数据:

// src/queries/useSomeDataQuery.ts

import {useQuery} from 'react-query';
import {SomeDataType} from '../store/index';

const QUERY_KEY_OF_SOME_DATA_QUERY = 'QUERY_KEY_OF_SOME_DATA_QUERY';

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

在 ComponentA 中调用 useSomeDataQuery 拉取数据并存至 store 中:

// src/components/ComponentA.tsx

import React, {FC, useEffect} from 'react';
import {useDispatch} from 'react-redux';
import {useSomeDataQuery} from '../queries/useSomeDataQuery';
import {setSomeData} from '../store/action';

const ComponentA: FC = () => {
  const dispatch = useDispatch();

  const params = {
    /* 在 ComponentA 中准备用于请求数据的参数 */
  };
  const {data, refetch} = useSomeDataQuery(params);

  useEffect(() => {
    dispatch(setSomeData(data));
  }, [data]);

  return (
    <div>
      <button onClick={() => refetch()}>Refresh data</button>
    </div>
  );
};

在 ComponentB 中获取 store 中的数据:

// src/components/ComponentB.tsx

import React, {FC} from 'react';
import {useSelector} from 'react-redux';

const ComponentB: FC = () => {
  // Tips: 我们也可以使用同样的 useSomeDataQuery 在 ComponentB 中获取数据,但在 query
  // 所需的 params 比较复杂时,这种做法成本会很高。
  const someData = useSelector((state) => state.global.someData);
  return <div>{/* 使用 someData 渲染 UI */}</div>;
};

所以你最终的项目代码目录可能是这个结构:

src
-- components
  -- ComponentA.tsx
  -- ComponentB.tsx
-- queries
  -- useSomeDataQuery.ts
-- store
  -- index.ts
  -- action.ts
  -- reducer.ts

而事实上我们想做的事情,只是想将 useSomeDataQuery 获取的数据 someData,放进 redux 的 store 中,并在不同的 React 组件内共享而已。

显而易见,这样的目录结构并不直观,因为我们仅仅为了传播一个数据而不得不在不同的目录文件下新增或修改代码,无论是资深的前端工程师还是初学者都该能意识到,这是不好的编码和阅读体验。

我们期望的效果是一处修改,随处可用

使用 Spreado 之后的目录结构

如果是使用 Spreado,除去必要的组件外,我们只需要修改一个文件就可以了:

// src/queries/useSomeDataQuery.ts

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

const INDEX_OF_SOME_DATA_QUERY = 'INDEX_OF_SOME_DATA_QUERY';

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

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

在 ComponentA 中调用 useSpreadOut hook 拉取数据:

// src/components/ComponentA.tsx

import React, {FC, useEffect} from 'react';
import {useSomeDataQuerySpreadOut} from '../queries/useSomeDataSpreado';

const ComponentA: FC = () => {
  const params = {
    /* 准备用于请求数据的参数 */
  };
  const {data: someData, refetch} = useSomeDataQuerySpreadOut(params);

  return (
    <div>
      <button onClick={() => refetch()}>Refresh data</button>
    </div>
  );
};

在 ComponentB 中调用 useSpreadIn hook 直接使用:

// src/components/ComponentB.tsx

import React, {FC} from 'react';
import {useSomeDataQuerySpreadIn} from '../queries/useSomeDataSpreado';

const ComponentB: FC = () => {
  const {data: someData} = useSomeDataQuerySpreadIn();
  return <div>{/* 使用 someData 渲染 UI */}</div>;
};

由于我们在拉取数据之后使用 spreado 将结果直接放入了 redux 的 store 中,所以我们的代码结构可以简化为:

src
-- components
  -- ComponentA.tsx
  -- ComponentB.tsx
-- queries
  -- useSomeDataQuery.ts

对于目录结构的瘦身效果是显而易见的。

其他组合

Spreado 支持的主流状态管理库和数据拉取库中,我们遵循同样的使用方法,所以这种对目录结构的瘦身效果也是完全一样的,一些初学者甚至可以一定程度忽略掉类似 redux 这样的状态管理库的学习成本,只需要知道 它们 可以存储和传播状态,而不需要深入去了解 action、reducer 以及 store 这些具体概念。

写在最后

如果有任何的疑问或想法,欢迎在 spreado/issues 留言,中英文都可以。如果有时间和兴趣,欢迎直接提交代码,具体可以参考开发引导。如果觉得小工具有帮助,请给 GitHub repo react-easier/spreado 点个 ⭐️,这也是我们不断前行的动力 😃。