代码重构——表格组件的思考

背景

在进行需求开发时,看到了几段令我浑身难受的代码,先上图给大家看看。

const [total, setTotal] = useState(0);

const [data, setData] = useState([]);

const [currentPage, setCurrentPage] = useState(INIT_PAGE);

const [isLoading, setIsLoading] = useState(false);

const [scrollHeight, setScrollHeight] = useState('');

useLayoutEffect(() => {
  setScrollHeight(computedTableHeight());
}, [])

这些代码的作用是控制 Table 组件的相关状态(如数据源、总数、当前页码、加载、表格的固定高度等),这些状态在许多表格中都有用到。因此,不出意外的,这段代码重复出现在了多个包含表格的文件中。

那么,秉承着 DRY 的原则,我对这个表格的状态管理进行了一些封装,让我们往下看。

方案一:自定义 Hook

一提到可重用的组件状态管理,第一个出现在我脑海中的方案就是「自定义 Hook」。相信很多了解 React Hooks 的朋友们对它已经很熟悉了,自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。
说白了,在这个场景下,就是将统一的的状态管理逻辑,封装到一个公共函数中。那么上述提到的表格的统一状态,都可以封装到同一个表格中,我们的自定义函数看起来就像下面这样:

export const useTable = () => {
  const [total, setTotal] = useState(0);

  const [data, setData] = useState([]);

  const [currentPage, setCurrentPage] = useState(INIT_PAGE);

  const [isLoading, setIsLoading] = useState(false);

  const [scrollHeight, setScrollHeight] = useState('');

  // 在这里可以做一些统一的逻辑处理,比如在初始化时调整表格的高度
  useLayoutEffect(() => {
    setScrollHeight(computedTableHeight());
  }, [])

  // 更进一步的,可以做一些复杂的逻辑封装,比如远程获取数据,并改变表格的状态等
    
  // 暴露出状态和修改状态的方法,这里只是简单地将所有内容暴露出去
  return {
    tableState: {
      total,
      data,
      currentPage,
      isLoading,
      scrollHeight
    },
    setTableState: {
      setTotal,
      setData,
      setCurrentPage,
      setIsLoading,
      setScrollHeight,
    }
  };
}

那么,有了上面这个简单的 useTable,我们在表格中就可以这样使用这个自定义 Hook:

const { tableState, setTableState } = useTable();

方案二:useReducer

在这里我想提一个容易被人忽略的官方 Hook —— useReducer。由于大多数人对 useState 的偏爱和习惯,useReducer 的出场率低的可怜,比如在这个项目中,useState 这个词出现了 169 次,而作为对比,useReducer 出现的次数是 0!
关于 useReducer 与 useState 的比较,可以参考《区别》和《何时使用》这两篇文章。课代表在这里简单总结一下它俩的关系:

  1. useReducer 与 useState 都可以用来做状态管理,实际上,查看源码我们能够看出,在 React 内部,useState 就是用 useReducer 实现的,useState 返回的函数内部封装了一个 dispatch
/**
 * useState 源码
 */

function basicStateReducer(state: S, action: BasicStateAction): S {
  return typeof action === 'function' ? action(state) : action;
}

export function useState(
  initialState: (() => S) | S,
): [S, Dispatch>] {
  return useReducer(
    basicStateReducer,
    // useReducer has a special case to support lazy useState initializers
    (initialState: any),
  );
}
  1. useState 适合用来做细粒度、简单类型(比如 number、string、boolean)的状态管理
  2. useReducer 适合用来做低成本的数据流,也可以用来管理较为复杂(object、array)、有关联的状态
  3. useReducer 常与 useContext 搭配使用,适合用来做简易的组件间数据流管理

那么回到这个场景,我们使用 useReducer 改造一下上述代码,看起来就像:

interface TableAction {
  type: TableActionType;
  payload: any;
}

export enum TableActionType {
  UPDATE_TOTAL = 'UPDATE_TOTAL',
  UPDATE_DATA = 'UPDATE_DATA',
  UPDATE_CURRENT_PAGE = 'UPDATE_CURRENT_PAGE',
  UPDATE_SCROLL_HEIGHT = 'UPDATE_SCROLL_HEIGHT',
  UPDATE_IS_LOADING = 'UPDATE_IS_LOADING'
}

export const initTableState: TableState = {
  total: 0,
  data: [],
  currentPage: INIT_PAGE,
  isLoading: false,
  scrollHeight: '',
};

export const tableReducer = (state: TableState, action: TableAction) => {
  switch (action.type) {
    case TableActionType.UPDATE_TOTAL:
      return {
        ...state,
        total: action.payload,
      };
    case TableActionType.UPDATE_DATA:
      return {
        ...state,
        data: [...action.payload],
      };
    case TableActionType.UPDATE_CURRENT_PAGE:
      return {
        ...state,
        currentPage: action.payload,
      };
    case TableActionType.UPDATE_SCROLL_HEIGHT:
      return {
        ...state,
        scrollHeight: action.payload,
      };
    case TableActionType.UPDATE_IS_LOADING:
      return {
        ...state,
        isLoading: action.payload,
      };
    default:
      return state;
  }
};

在表格组件中使用的姿势如下:

const [tableState, dispatch] = useReducer(tableReducer, initTableState);
const { total, data, currentPage, isLoading, scrollHeight } = tableState;

useLayoutEffect(() => {
  dispatch({ type: TableActionType.UPDATE_SCROLL_HEIGHT, payload: computedTableHeight() });
}, []);

进阶:高级表格

上面两种方案都是为了解决表格组件内的冗杂状态问题,那么在此基础之上,我们是否能提高抽象的层级,将重复的逻辑范围搜索扩大到组件之间。
作为后台应用,下图是十分常见的列表页布局:(筛选查询表单 + 标题操作区 +展示表格 + 分页栏)。

列表页布局

基于这类布局的列表页面,我们不难想到,将筛选搜索、标题操作、分页操作等一系列常用的功能封装成组件的配置化参数的能力。
那么这个页面的代码看起来就像是:


  columns= { columns }
  actionRef = { actionRef }
  request = { async(params = {}, sort, filter) => {
    return request<{
      data: CustomItem[];
    }>('https://proapi.azurewebsites.net/github/issues', {
      params,
    });
  }}
  columnsState = {{
    persistenceKey: 'pro-table-singe-demos',
      persistenceType: 'localStorage',
        onChange(value) {
      console.log('value: ', value);
    },
  }}
  rowKey = "id"
  search = {{
    labelWidth: 'auto',
  }}
  form = {{
    // 由于配置了 transform,提交的参与与定义的不同这里需要转化一下
    syncToUrl: (values, type) => {
      if (type === 'get') {
        return {
          ...values,
          created_at: [values.startTime, values.endTime],
        };
      }
      return values;
    },
  }}
  pagination = {{
    pageSize: 5,
    onChange: (page) => console.log(page),
  }}
  dateFormatter = "string"
  headerTitle = "高级表格"
  toolBarRender = {() => [
    ,
    
      
    ,
  ]}
/>

只需要数十行代码,就能渲染出这样一个列表页。

总结

不论是使用自定义 Hook 还是使用 useReducer,其主旨都是将多个组件之间重复的逻辑进行抽离,以此达到 Don't Repeat Yourself 的原则。

你可能感兴趣的:(代码重构——表格组件的思考)