React Hook 系列(二):自定義hook的一些實踐

——Smartisan Pro 2S 攝於·北京react

前言

從React 16.8 穩定版hook發佈近一年多,使用hook並不廣泛,緣由可能有兩方面: 1、官方並無徹底取代class;2、迭代項目徹底hook話須要成本,官方也不推薦。恰巧新項目伊始,就全面採用hook,這也是寫這篇文章的起因,接上一篇 ,這篇主要是自定義hook的一些實踐, 不必定是最佳,但願個人一點分享總結,能給認真閱讀的你帶來收益。源碼在這,,,在線demogit

正文

下面是項目中一些有表明性的hook,目前也是項目中的一些最佳實踐。github

🐤 1. HOC 到 Render Props 再到 hook

業務代碼經常使用實現雙向綁定, 分別用以上三種實現,以下:json

HOC寫法redux

const HocBind = WrapperComponent =>
  class extends React.Component {
    state = {
      value: this.props.initialValue
    };
    onChange = e => {
      this.setState({ value: e.target.value });
      if (this.props.onChange) {
        this.props.onChange(e.target.value);
      }
    };
    render() {
      const newProps = {
        value: this.state.value,
        onChange: this.onChange
      };
      return <WrapperComponent {...newProps} />;
    }
  };
// 用法
const Input = props => (
  <>
    <p>HocBind實現 value:{props.value}</p>
    <input placeholder="input" {...props} />
  </>
);
<HocInput
  initialValue="init"
  onChange={val => {
    console.log("HocInput", val);
  }}
/>
複製代碼

Render Props寫法api

// props 兩個參數initialValue 輸入,onChange輸出
class HocBind extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: props.initialValue
    };
  }
  onChange = e => {
    this.setState({ value: e.target.value });
    if (this.props.onChange) {
      this.props.onChange(e.target.value);
    }
  };
  render() {
    return (
      <>
        {this.props.children({
          value: this.state.value,
          onChange: this.onChange
        })}
      </>
    );
  }
}
// 用法
<HocBind
  initialValue="init"
  onChange={val => {
    console.log("HocBind", val);
  }}
>
  {props => (
    <>
      <p>HocBind實現 value:{props.value}</p>
      <input placeholder="input" {...props} />
    </>
  )}
</HocBind>
複製代碼

再看hook寫法瀏覽器

// initialValue默認輸入
function useBind(initialValue) {
  const [value, setValue] = useState(initialValue || "");
  const onChange = e => {
    setValue(e.target.value);
  };
  return { value, onChange };
}
// 用法
function InputBind() {
  const inputProps = useBind("init");
  return (
    <p> <p>useBind實現 value:{inputProps.value}</p> <input {...inputProps} /> </p> ); } 複製代碼

比較發現,HOC和render props方式都會侵入代碼,使得代碼閱讀性降低,也不夠優雅,組件內部暴露的value值,在外部也很難拿到, 反觀 hook 的寫法,邏輯徹底解耦,使用場景最大化且不侵入代碼,在組件頂層能夠拿到雙向綁定的值,比以前優雅不少。 源碼緩存

總結bash

  • hook 可讀性高,也易於維護。
  • hook 不會侵入代碼, 不會形成嵌套。
  • hook UI和邏輯完全拆分,更容易複用。

🐤 2. 擺脫重複fetch, 自定義useFetch

fetch數據基本是最多見的須要封裝邏輯,先看看我初版的useFetch微信

function useFetch(fetch, params) {
  const [data, setData] = useState({});

  const fetchApi = useCallback(async () => {
    const res = await fetch(params);
    if (res.code === 1) {
      setData(res.data);
    }
  }, [fetch, params]);

  useEffect(() => {
    fetchApi();
  }, [fetchApi]);

  return data;
}
// 用法
import { getSsq } from "../api";
function Ssq() {
  const data = useFetch(getSsq, { code: "ssq" });
  return <div>雙色球開獎號碼:{data.openCode}</div>;
}
// api導出方法
export const getSsq = params => {
  const url =
    "https://www.mxnzp.com/api/lottery/common/latest?" + objToString(params);
  return fetch(url).then(res => res.json());
};
複製代碼

結果: CPU爆表💥,瀏覽器陷入死循環,思考一下, why?

fix bug開始,更改一下調用方式:

...
const params = useMemo(() => ({ code: "ssq" }), []);  
const data = useFetch(getSsq, params);
...
複製代碼

🤡驚訝,是想要的結果,可是,why?(若是你不知道,歡迎查閱[React Hook 系列一),由於調用useFetch(getSsq, { code: "ssq" }); 第二個參數在useFetch中被useCallback依賴,頁面的執行過程:render => 執行useEffect => 調用useCallback方法 => 更新data => render => useEffect => 調用useCallback方法 判斷依賴是否變化 肯定是否跳過此次執行 ... ,對於useCallback 來講 params 對象每次都是新的對象, 因此這個渲染流程會一直執行,形成死循環。useMemo的做用就是幫你緩存params且返回一個memoized的值, 當useMemo的依賴值沒有變化,memoized就是不變的,因此useCallback會跳過這次執行。

你覺得就這樣結束了?

詭異的微笑😎😜,每次在使用useFetch都須要用useMemo包裹params,一點兒也不優雅,再改改?

要解決的問題:如何保持params不變時,保持惟一?

首先想到JSON.stringify,碼上const data = useFetch(getSsq, JSON.stringify({ code: "ssq" })), 再見吧,煩人的對象,每當參數不變時他就是個不變的字符串,在fetch傳入的時候JSON.parse(params), 🤩好機智。可是好像哪裏不對, 這要是被大佬看到,大佬: 「emmmm,你這仍是不夠優雅,雖然問題解決了,再改改?「,我說:」嗯!!!「。

useState, 對就是他, 他能夠緩存params,通過他包裹的,當他沒有變化時useCallback和useEffect都認爲他是不變的,會跳過執行回調,因而乎useFetch變成了如下樣子:

function useFetch(fetch, params) {
  const [data, setData] = useState({});
  const [newParams] = useState(params);
  const fetchApi = useCallback(async () => {
    console.log("useCallback");
    const res = await fetch(newParams);
    if (res.code === 1) {
      setData(res.data);
    }
  }, [fetch, newParams]);

  useEffect(() => {
    console.log("useEffect");
    fetchApi();
  }, [fetchApi]);

  return data;
}
// 調用
const data = useFetch(getSsq, { code: "ssq" });
複製代碼

👏👏👏欣喜若狂。

我: 」大佬, 這樣好像沒啥問題了「。

大佬:」emmm, 我要更新如下參數,還會fetch數據嗎?「

我: 」嗯?!?「

大佬: "你再看看?"

我:」好(怯怯的說,注意讀 輕聲)「

那好,不就是想更新params嗎,更新確定是用戶的操做, 多以暴露更新newParams的方法就OK吧,因而:

function useFetch(fetch, params) {
  ...
  const doFetch = useCallback(rest => {
    setNewParams(rest);
  }, []);
  return { data, doFetch };
}
// 調用
const { data, doFetch } = useFetch(getSsq, { code: "ssq" });
console.log("render");
return (
  <div> 開獎號碼:{data.openCode} <button onClick={() => doFetch({ code: "fc3d" })}>福彩3D</button> </div>
);
複製代碼

🙃🙂🙃🙂淡定微笑。

不行,此次不能讓大佬說 你在看看吧, 我必須未雨綢繆,fetch數據的場景我必需分析一下:

  • 頁面首次進入或刷新。
  • 用戶改變fetch數據的參數時。
  • 用戶點擊modal後加載數據, 或者當請求參數依賴某個數據的有無纔會fetch數據。
  • 在不改變參數的狀況下,用戶手動點擊刷新頁面按鈕。
  • fetch數據時頁面loading。

第3、4、五果真不知足,辛虧啊。。。差點又🐶, 因而5分鐘後:

function useFetch(fetch, params, visible = true) {
  const [data, setData] = useState({});
  const [loading, setLoading] = useState(false);
  const [newParams, setNewParams] = useState(params);
  const fetchApi = useCallback(async () => {
    console.log("useCallback");
    if (visible) {
      setLoading(true);
      const res = await fetch(newParams);
      if (res.code === 1) {
        setData(res.data);
      }
      setLoading(false);
    }
  }, [fetch, newParams, visible]);

  useEffect(() => {
    console.log("useEffect");
    fetchApi();
  }, [fetchApi]);

  const doFetch = useCallback(rest => {
    setNewParams(rest);
  }, []);

  const reFetch = () => {
    setNewParams(Object.assign({}, newParams));
  };
  return {
    loading,
    data,
    doFetch,
    reFetch
  };
}
複製代碼

最後大佬說,這個版本目前能知足業務的需求,先用用看,emmmm,🍻🍻🍻。源碼

可是useFetch還能夠封裝的更健壯,不須要傳入api方法,直接將fetch的參數以及過程封裝起來,系列文章寫完,計劃基於原生fetch封裝 useFetch 輪子, 期待ing...

🐤 3. 扯淡的table,自定義useTable

爲何要寫這個hook哪, 先看看沒有useTable以前的代碼,前提咱們使用了ant-design。

const rowSelection = {
  selectedRowKeys,
  onChange: this.onSelectChange,
};
<Table rowKey="manage_ip" pagination={{ ...pagination, total, current: pagination.page, }} onChange={p => { getSearchList({ page: p.current, pageSize: p.pageSize }); }} rowSelection={rowSelection} loading={{ spinning: loading.OperationComputeList, delay: TABLE_DELAY }} columns={columns} dataSource={list} /> 複製代碼

哇,相似中臺系統,每一個頁面基本都有個table,且都長得很類似,重複代碼有點多,因而乎開始想如何偷懶。

首先有table的每一個頁面基本都涉及到分頁,都是重複的邏輯,因此先搞個usePagination來處理分頁的邏輯達到複用,輸入值爲默認值,暴露change供用戶操做,那麼:

export const defaultPagination = {
  pageSize: 10,
  current: 1
};
function usePagination(config = defaultPagination) {
  const [pagination, setPagination] = useState({
    pageSize: config.pageSize || defaultPagination.pageSize,
    current: config.page || config.defaultCurrent || defaultPagination.current
  });

  const paginationConfig = useMemo(() => {
    return {
      ...defaultPagination,
      showTotal: total =>
        `每頁 ${pagination.pageSize} 條 第 ${pagination.current}頁 共 ${total}`,
      ...config,
      pageSize: pagination.pageSize,
      current: pagination.current,
      onChange: (current, pageSize) => {
        if (config.onChange) {
          config.onChange(current, pageSize);
        }
        setPagination({ pageSize, current });
      },
      onShowSizeChange: (current, pageSize) => {
        if (config.onChange) {
          config.onChange(current, pageSize);
        }
        setPagination({ pageSize, current });
      }
    };
  }, [config, pagination]);

  return paginationConfig;
}
複製代碼

以上用戶的操做邏輯和change後的動做解耦, total做爲fetch後動態變化的,因此不能省略。嘗試直接在Pagination組件中使用,也沒有問題。

同理rowSelection做爲公共的邏輯,也能夠按照以上的邏輯,將其自定義成hook:

const { rowSelection, selectedList, selectedRowKey, resetSelection } = useRowSelection(options);
// options 爲rowSelection的全部屬性,可不輸入。
// rowSelection, selectedList, selectedRowKey爲暴露屬性和已選數據。
// resetSelection 取消全部選中
複製代碼

就長這樣,很簡單:

function useRowSelection(options = {}) {
  const [selectedList, setSelectedList] = useState(options.selectedList || []);
  const [selectedRowKey, setSelectedRowKeys] = useState(
    options.selectedRowKey || []
  );
  const rowSelection = useMemo(() => {
    return {
      columnWidth: "44px",
      ...options,
      selectedList,
      selectedRowKey,
      onChange: (selectedRowKeys, selectedRows) => {
        setSelectedRowKeys(selectedRowKeys);
        setSelectedList(selectedRows);
        if (options.onChange) {
          options.onChange(selectedRowKeys, selectedRows);
        }
      }
    };
    // 操做完取消選中
    const resetSelection = useCallback(() => {
      setSelectedList([]);
      setSelectedRowKeys([]);
    }, []);
  }, [selectedList, selectedRowKey, options]);
  return { rowSelection, selectedList, selectedRowKey, resetSelection };
}
複製代碼

最終table用起來可能長這樣:

const { data = {}, loading, doFetch } = useFetch(getJokes, {
  page: 1
});
const pagination = usePagination({
  total: data.totalCount,
  onChange: (page, limit) => {
    doFetch({ page, limit });
  }
});
const { rowSelection, selectedList, selectedRowKey, resetSelection } = useRowSelection();
const columns = [
  { title: "笑話內容", dataIndex: "content" },
  { title: "更新時間", dataIndex: "updateTime" }
];
console.log("render");
return (
  <Table rowKey="content" loading={loading} pagination={pagination} rowSelection={rowSelection} columns={columns} dataSource={data.list} /> ); 複製代碼

👎👊👎不是說好的useTable嗎, 如今也沒見着我的影啊,好好下面開始useTable的由來。

通過觀察pagination和dataSource都依賴fetch後的數據, 因此fetch的過程能夠放在useTable中,rowSelection也只需返回值配置項便可,只有columns和rowKey是依賴頁面的業務邏輯不須要封裝,須要用戶操做的只需暴露交給用戶,其餘的只是返回默認值便可,那useTable 的樣子大概出來了:

const [tableProps, resetSelection, selectedList, selectedRowKey] = useTable({
  fetch:fetchData
  params: {},
	pagination: {
		// init
		onChange: () => {...},
	},
	rowSelection: {
		// init
		onChange: () => {...},
	},
});
<Table rowKey='id' columns={columns} {...tableProps} /> 複製代碼

不過 table 還須要能filter,幸虧table有個onChange的API 暴露了分頁搜索和排序的全部響應,因此:

import { useCallback } from "react";
import usePagination, { defaultPagination } from "./use-pagination";
import useFetch from "./use-fetch";
import useRowSelection from "./use-row-selection";

function useTable(options) {
  const { data = {}, loading, doFetch: dofetch, reFetch } = useFetch(
    options.fetch,
    {
      ...defaultPagination,
      ...options.params
    }
  );

  const tableProps = {
    dataSource: data.list,
    loading,
    onChange: (
      pagination,
      filters,
      sorter,
      extra: { currentDataSource: [] }
    ) => {
      if (options.onChange) {
        options.onChange(pagination, filters, sorter, extra);
      }
    }
  };

  const { paginationConfig, setPagination } = usePagination({
    total: data.totalCount,
    ...(options.pagination || {}),
    onChange: (page, pageSize) => {
      if (!options.onChange) {
        if (options.pagination && options.pagination.onChange) {
          options.pagination.onChange(page, pageSize);
        } else {
          doFetch({ page, pageSize });
        }
      }
    }
  });
  if (options.pagination === false) {
    tableProps.pagination = false;
  } else {
    tableProps.pagination = paginationConfig;
  }

  const {
    rowSelection,
    selectedList,
    selectedRowKeys,
    resetSelection
  } = useRowSelection(
    typeof options.rowSelection === "object" ? options.rowSelection : {}
  );
  if (options.rowSelection) {
    tableProps.rowSelection = rowSelection;
  }

  const doFetch = useCallback(
    params => {
      dofetch(params);
      if (params.page) {
        setPagination({
          pageSize: paginationConfig.pageSize,
          current: params.page
        });
      }
    },
    [paginationConfig, setPagination, dofetch]
  );

  return {
    tableProps,
    resetSelection,
    selectedList,
    selectedRowKeys,
    doFetch,
    reFetch
  };
}
export default useTable;

// 用法
const {
  tableProps,
  resetSelection,
  selectedList,
  selectedRowKeys,
  doFetch,
  reFetch
} = useTable({
  fetch: getJokes,
  params: null,
  onChange: (
    pagination,
    filters,
    sorter,
    extra: { currentDataSource: [] }
  ) => {
  // doFetch({ page: pagination.current, ...filters });
  console.log("onChange", pagination, filters, sorter, extra);
}
// pagination: false
// pagination: true
// pagination: {
// onChange: (page, pageSize) => {
// console.log("pagination", page, pageSize);
// doFetch({ page, pageSize });
// }
// },
// rowSelection: false,
// rowSelection: true
// rowSelection: {
// onChange: (rowKey, rows) => {
// console.log("rowSelection", rowKey, rows);
// }
// }
});

<Table rowKey="content" columns={columns} {...tableProps} /> 複製代碼

以上能夠知足目前業務關於table的全部需求,歡迎來踩。

總結:

  • 不要試圖在公共組件寫入業務邏輯(也不要試圖猜想用戶的操做後的相應)。
  • 健壯的組件須要默認值,也容許用戶去修改默認值。

🐤 4. 其餘

其餘與頁面反作用相關的工具函數均可以抽象成hook, 例如基於Rxjsuse-observable,定時器 use-interval ,基於localStorage 的封裝 use-localStorage,基於Form的use-form, 基於Modal的use-modal 等等。

🐤 5. 總結

hook 真香🤡🤡,代碼可讀性提升,比HOC、Render Props更優雅, UI和邏輯耦合度更低,組件複用程度趨於最大化;固然Hook也不是萬能的,複雜的數據管理還須要相似redux的工具,譬如redux 有 middleware, 而hook沒有, 因此技術選型還需從業務的角度去衡量。一點總結以下:

  • 通常自定義 hook 只負責邏輯,不負責渲染。
  • 公共邏輯 hook儘可能細分,按照組件的單一原則劃分,單一hook只負責單一的職責。
  • 複雜的計算能夠考慮用 useCallback, useMemo 去優化。

微信:gwt385260 歡迎交流🤝🤝🤝~

掘金年度徵文 | 2019 與個人技術之路 徵文活動正在進行中......

相關文章
相關標籤/搜索