列表頁常見hook封裝實例

引言

本文是深入淺出 ahooks 源碼系列文章,這個系列的目標主要有以下幾點:

  • 加深對 React hooks 的理解。
  • 學習如何抽象自定義 hooks。構建屬於自己的 React hooks 工具庫。
  • 培養閱讀學習源碼的習慣,工具庫是一個對源碼閱讀不錯的選擇。

列表頁常見元素

對於一些後臺管理系統,典型的列表頁包括篩選表單項、Table表格、Pagination分頁這三部分。

針對使用 Antd 的系統,在 ahooks 中主要是通過 useAntdTable 和 usePagination 這兩個 hook 來封裝。

usePagination

usePagination 基於 useRequest 實現,封裝瞭常見的分頁邏輯。

首先通過 useRequest 處理請求,service 約定返回的數據結構為 { total: number, list: Item[] }

其中 useRequest 的 defaultParams 參數第一個參數為 { current: number, pageSize: number }。並根據請求的參數以及返回的 total 值,得出總的頁數。

還有 refreshDeps 變化,會重置 current 到第一頁「changeCurrent(1)」,並重新發起請求,一般你可以把 pagination 依賴的條件放這裡。

const usePagination = <TData extends Data, TParams extends Params>(
  service: Service<TData, TParams>,
  options: PaginationOptions<TData, TParams> = {},
) => {
  const { defaultPageSize = 10, ...rest } = options;
  // service 返回的數據結構為 { total: number, list: Item[] }
  const result = useRequest(service, {
    // service 的第一個參數為 { current: number, pageSize: number }
    defaultParams: [{ current: 1, pageSize: defaultPageSize }],
    // refreshDeps 變化,會重置 current 到第一頁,並重新發起請求,一般你可以把 pagination 依賴的條件放這裡
    refreshDepsAction: () => {
      // eslint-disable-next-line @typescript-eslint/no-use-before-define
      changeCurrent(1);
    },
    ...rest,
  });
    // 取到相關的請求參數
  const { current = 1, pageSize = defaultPageSize } = result.params[0] || {};
  // 獲取請求結果,total 代表數據總條數
  const total = result.data?.total || 0;
  // 獲取到總的頁數
  const totalPage = useMemo(() => Math.ceil(total / pageSize), [pageSize, total]);
}

重點看下 onChange 方法:

  • 入參分別為當前頁數以及當前每一頁的最大數量。
  • 根據 total 算出總頁數。
  • 獲取到所有的參數,執行請求邏輯。
  • 當修改當前頁或者當前每一頁的最大數量的時候,直接調用 onChange 方法。
// c,代表 current page
// p,代表 page size
const onChange = (c: number, p: number) => {
  let toCurrent = c <= 0 ? 1 : c;
  const toPageSize = p <= 0 ? 1 : p;
  // 根據 total 算出總頁數
  const tempTotalPage = Math.ceil(total / toPageSize);
  // 假如此時總頁面小於當前頁面,需要將當前頁面賦值為總頁數
  if (toCurrent > tempTotalPage) {
    toCurrent = Math.max(1, tempTotalPage);
  }
  const [oldPaginationParams = {}, ...restParams] = result.params || [];
  // 重新執行請求
  result.run(
    // 留意參數變化,主要是當前頁數和每頁的總數量發生變化
    {
      ...oldPaginationParams,
      current: toCurrent,
      pageSize: toPageSize,
    },
    ...restParams,
  );
};
const changeCurrent = (c: number) => {
  onChange(c, pageSize);
};
const changePageSize = (p: number) => {
  onChange(current, p);
};

最後返回請求的結果以及 pagination 字段,包含所有分頁信息。另外還有操作分頁的函數。

return {
  ...result,
  // 會額外返回 pagination 字段,包含所有分頁信息,及操作分頁的函數。
  pagination: {
    current,
    pageSize,
    total,
    totalPage,
    onChange: useMemoizedFn(onChange),
    changeCurrent: useMemoizedFn(changeCurrent),
    changePageSize: useMemoizedFn(changePageSize),
  },
} as PaginationResult<TData, TParams>;

小結:usePagination 默認用法與 useRequest 一致,但內部封裝瞭分頁請求相關的邏輯。返回的結果多返回一個 pagination 參數,包含所有分頁信息,及操作分頁的函數。

缺點就是對 API 請求參數有所限制,比如入參結構必須為 { current: number, pageSize: number },返回結果為 { total: number, list: Item[] }

useAntdTable

useAntdTable 基於 useRequest 實現,封裝瞭常用的 Ant Design Form 與 Ant Design Table 聯動邏輯,並且同時支持 antd v3 和 v4。

首先調用 usePagination 處理分頁的邏輯。

const useAntdTable = &lt;TData extends Data, TParams extends Params&gt;(
  service: Service&lt;TData, TParams&gt;,
  options: AntdTableOptions&lt;TData, TParams&gt; = {},
) =&gt; {
  const {
    // form 實例
    form,
    // 默認表單選項
    defaultType = 'simple',
    // 默認參數,第一項為分頁數據,第二項為表單數據。[pagination, formData]
    defaultParams,
    manual = false,
    // refreshDeps 變化,會重置 current 到第一頁,並重新發起請求。
    refreshDeps = [],
    ready = true,
    ...rest
  } = options;
  // 對分頁的邏輯進行處理
  // 分頁也是對 useRequest 的再封裝
  const result = usePagination&lt;TData, TParams&gt;(service, {
    manual: true,
    ...rest,
  });
  // ...
}

然後處理列表頁篩選 Form 表單的邏輯,這裡支持 Antd v3 和 Antd v4 版本。

// 判斷是否為 Antd 的第四版本
const isAntdV4 = !!form?.getInternalHooks;

獲取當前表單值,form.getFieldsValue 或者 form.getFieldInstance

// 獲取當前的 from 值
const getActivetFieldValues = () => {
  if (!form) {
    return {};
  }
  // antd 4
  if (isAntdV4) {
    return form.getFieldsValue(null, () => true);
  }
  // antd 3
  const allFieldsValue = form.getFieldsValue();
  const activeFieldsValue = {};
  Object.keys(allFieldsValue).forEach((key: string) => {
    if (form.getFieldInstance ? form.getFieldInstance(key) : true) {
      activeFieldsValue[key] = allFieldsValue[key];
    }
  });
  return activeFieldsValue;
};

校驗表單邏輯 form.validateFields:

// 校驗邏輯
const validateFields = (): Promise<Record<string, any>> => {
  if (!form) {
    return Promise.resolve({});
  }
  const activeFieldsValue = getActivetFieldValues();
  const fields = Object.keys(activeFieldsValue);
  // antd 4
  // validateFields 直接調用
  if (isAntdV4) {
    return (form.validateFields as Antd4ValidateFields)(fields);
  }
  // antd 3
  return new Promise((resolve, reject) => {
    form.validateFields(fields, (errors, values) => {
      if (errors) {
        reject(errors);
      } else {
        resolve(values);
      }
    });
  });
};

重置表單 form.setFieldsValue

// 重置表單
const restoreForm = () => {
  if (!form) {
    return;
  }
  // antd v4
  if (isAntdV4) {
    return form.setFieldsValue(allFormDataRef.current);
  }
  // antd v3
  const activeFieldsValue = {};
  Object.keys(allFormDataRef.current).forEach((key) => {
    if (form.getFieldInstance ? form.getFieldInstance(key) : true) {
      activeFieldsValue[key] = allFormDataRef.current[key];
    }
  });
  form.setFieldsValue(activeFieldsValue);
};

修改表單類型,支持 'simple''advance'。初始化的表單數據可以填寫 simple 和 advance 全量的表單數據,開發者可以根據當前激活的類型來設置表單數據。修改 type 的時候會重置 form 表單數據。

const changeType = () => {
  // 獲取當前表單值
  const activeFieldsValue = getActivetFieldValues();
  // 修改表單值
  allFormDataRef.current = {
    ...allFormDataRef.current,
    ...activeFieldsValue,
  };
  // 設置表單類型
  setType((t) => (t === 'simple' ? 'advance' : 'simple'));
};
// 修改 type,則重置 form 表單數據
useUpdateEffect(() => {
  if (!ready) {
    return;
  }
  restoreForm();
}, [type]);

_submit 方法:對 form 表單校驗後,根據當前 form 表單數據、分頁等篩選條件進行對表格數據搜索。

const _submit = (initPagination?: TParams[0]) => {
  setTimeout(() => {
    // 先進行校驗
    validateFields()
      .then((values = {}) => {
        // 分頁的邏輯
        const pagination = initPagination || {
          pageSize: options.defaultPageSize || 10,
          ...(params?.[0] || {}),
          current: 1,
        };
        // 假如沒有 form,則直接根據分頁的邏輯進行請求
        if (!form) {
          // @ts-ignore
          run(pagination);
          return;
        }
        // 獲取到當前所有 form 的 Data 參數
        // record all form data
        allFormDataRef.current = {
          ...allFormDataRef.current,
          ...values,
        };
        // @ts-ignore
        run(pagination, values, {
          allFormData: allFormDataRef.current,
          type,
        });
      })
      .catch((err) => err);
  });
};

另外當表格觸發 onChange 方法的時候,也會進行請求:

// Table 組件的 onChange 事件
const onTableChange = (pagination: any, filters: any, sorter: any) => {
  const [oldPaginationParams, ...restParams] = params || [];
  run(
    // @ts-ignore
    {
      ...oldPaginationParams,
      current: pagination.current,
      pageSize: pagination.pageSize,
      filters,
      sorter,
    },
    ...restParams,
  );
};

初始化的時候,會根據當前是否有緩存的數據,有則根據緩存的數據執行請求邏輯。否則,通過 manualready 判斷是否需要進行重置表單後執行請求邏輯。

// 初始化邏輯
// init
useEffect(() => {
  // if has cache, use cached params. ignore manual and ready.
  // params.length > 0,則說明有緩存
  if (params.length > 0) {
    // 使用緩存的數據
    allFormDataRef.current = cacheFormTableData?.allFormData || {};
    // 重置表單後執行請求
    restoreForm();
    // @ts-ignore
    run(...params);
    return;
  }
  // 非手動並且已經 ready,則執行 _submit
  if (!manual && ready) {
    allFormDataRef.current = defaultParams?.[1] || {};
    restoreForm();
    _submit(defaultParams?.[0]);
  }
}, []);

最後,將請求返回的數據通過 dataSource、 pagination、loading 透傳回給到 Table 組件,實現 Table 的數據以及狀態的展示。以及將對 Form 表單的一些操作方法暴露給開發者。

return {
  ...result,
  // Table 組件需要的數據,直接透傳給 Table 組件即可
  tableProps: {
    dataSource: result.data?.list || defaultDataSourceRef.current,
    loading: result.loading,
    onChange: useMemoizedFn(onTableChange),
    pagination: {
      current: result.pagination.current,
      pageSize: result.pagination.pageSize,
      total: result.pagination.total,
    },
  },
  search: {
    // 提交表單
    submit: useMemoizedFn(submit),
    // 當前表單類型, simple | advance
    type,
    // 切換表單類型
    changeType: useMemoizedFn(changeType),
    // 重置當前表單
    reset: useMemoizedFn(reset),
  },
} as AntdTableResult<TData, TParams>;

以上就是列表頁常見 hook 封裝示例的詳細內容,更多關於列表頁 hook 封裝的資料請關註WalkonNet其它相關文章!

推薦閱讀: