每天都在寫業務代碼中度過,但是呢,經常在寫業務代碼的時候,會感覺自己寫的某些代碼有點別扭,但是又不知道是哪里別扭,今天這篇文章我整理了一些在項目中使用的一些小的技巧點。
狀態邏輯復用
在使用React Hooks之前,我們一般復用的都是組件,對組件內部的狀態是沒辦法復用的,而React Hooks的推出很好的解決了狀態邏輯的復用,而在我們日常開發中能做到哪些狀態邏輯的復用呢?下面我羅列了幾個當前我在項目中用到的通用狀態復用。
useRequest
為什么要封裝這個hook呢?在數據加載的時候,有這么幾點是可以提取成共用邏輯的
- loading狀態復用
- 異常統一處理
- const useRequest = () => {
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState();
- const run = useCallback(async (...fns) => {
- setLoading(true);
- try {
- await Promise.all(
- fns.map((fn) => {
- if (typeof fn === 'function') {
- return fn();
- }
- return fn;
- })
- );
- } catch (error) {
- setError(error);
- } finally {
- setLoading(false);
- }
- }, []);
- return { loading, error, run };
- };
- function App() {
- const { loading, error, run } = useRequest();
- useEffect(() => {
- run(
- new Promise((resolve) => {
- setTimeout(() => {
- resolve();
- }, 2000);
- })
- );
- }, []);
- return (
- <div className="App">
- <Spin spinning={loading}>
- <Table columns={columns} dataSource={data}></Table>
- </Spin>
- </div>
- );
- }
usePagination
我們用表格的時候,一般都會用到分頁,通過將分頁封裝成hook,一是可以介紹前端代碼量,二是統一了前后端分頁的參數,也是對后端接口的一個約束。
- const usePagination = (
- initPage = {
- total: 0,
- current: 1,
- pageSize: 10,
- }
- ) => {
- const [pagination, setPagination] = useState(initPage);
- // 用于接口查詢數據時的請求參數
- const queryPagination = useMemo(
- () => ({ limit: pagination.pageSize, offset: pagination.current - 1 }),
- [pagination.current, pagination.pageSize]
- );
- const tablePagination = useMemo(() => {
- return {
- ...pagination,
- onChange: (page, pageSize) => {
- setPagination({
- ...pagination,
- current: page,
- pageSize,
- });
- },
- };
- }, [pagination]);
- const setTotal = useCallback((total) => {
- setPagination((prev) => ({
- ...prev,
- total,
- }));
- }, []);
- const setCurrent = useCallback((current) => {
- setPagination((prev) => ({
- ...prev,
- current,
- }));
- }, []);
- return {
- // 用于antd 表格使用
- pagination: tablePagination,
- // 用于接口查詢數據使用
- queryPagination,
- setTotal,
- setCurrent,
- };
- };
除了上面示例的兩個hook,其實自定義hook可以無處不在,只要有公共的邏輯可以被復用,都可以被定義為獨立的hook,然后在多個頁面或組件中使用,我們在使用redux,react-router的時候,也會用到它們提供的hook。
在合適場景給useState傳入函數
我們在使用useState的setState的時候,大部分時候都會給setState傳入一個值,但實際上setState不但可以傳入普通的數據,而且還可以傳入一個函數。下面極端代碼分別描述了幾個傳入函數的例子。
下面的代碼3秒后輸出什么?
如下代碼所示,也有有兩個按鈕,一個按鈕會在點擊后延遲三秒然后給count + 1, 第二個按鈕會在點擊的時候,直接給count + 1,那么假如我先點擊延遲的按鈕,然后多次點擊不延遲的按鈕,三秒鐘之后,count的值是多少?
- import { useState, useEffect } from 'react';
- function App() {
- const [count, setCount] = useState(0);
- function handleClick() {
- setTimeout(() => {
- setCount(count + 1);
- }, 3000);
- }
- function handleClickSync() {
- setCount(count + 1);
- }
- return (
- <div className="App">
- <div>count:{count}</div>
- <button onClick={handleClick}>延遲加一</button>
- <button onClick={handleClickSync}>加一</button>
- </div>
- );
- }
- export default App;
我們知道,React的函數式組件會在自己內部的狀態或外部傳入的props發生變化時,做重新渲染的動作。實際上這個重新渲染也就是重新執行這個函數式組件。
當我們點擊延遲按鈕的時候,因為count的值需要三秒后才會改變,這時候并不會重新渲染。然后再點擊直接加一按鈕,count值由1變成了2, 需要重新渲染。這里需要注意的是,雖然組件重新渲染了,但是setTimeout是在上一次渲染中被調用的,這也意味著setTimeout里面的count值是組件第一次渲染的值。
所以即使第二個按鈕加一多次,三秒之后,setTimeout回調執行的時候因為引用的count的值還是初始化的0, 所以三秒后count + 1的值就是1
如何讓上面的代碼延遲三秒后輸出正確的值?
這時候就需要使用到setState傳入函數的方式了,如下代碼:
- import { useState, useEffect } from 'react';
- function App() {
- const [count, setCount] = useState(0);
- function handleClick() {
- setTimeout(() => {
- setCount((prevCount) => prevCount + 1);
- }, 3000);
- }
- function handleClickSync() {
- setCount(count + 1);
- }
- return (
- <div className="App">
- <div>count:{count}</div>
- <button onClick={handleClick}>延遲加一</button>
- <button onClick={handleClickSync}>加一</button>
- </div>
- );
- }
- export default App;
從上面代碼可以看到,setCount(count + 1)被改為了setCount((prevCount) => prevCount + 1)。我們給setCount傳入一個函數,setCount會調用這個函數,并且將前一個狀態值作為參數傳入到函數中,這時候我們就可以在setTimeout里面拿到正確的值了。
還可以在useState初始化的時候傳入函數
看下面這個例子,我們有一個getColumns函數,會返回一個表格的所以列,同時有一個count狀態,每一秒加一一次。
- function App() {
- const columns = getColumns();
- const [count, setCount] = useState(0);
- useEffect(() => {
- setInterval(() => {
- setCount((prevCount) => prevCount + 1);
- }, 1000);
- }, []);
- useEffect(() => {
- console.log('columns發生了變化');
- }, [columns]);
- return (
- <div className="App">
- <div>count: {count}</div>
- <Table columns={columns}></Table>
- </div>
- );
- }
上面的代碼執行之后,會發現每次count發生變化的時候,都會打印出columns發生了變化,而columns發生變化便意味著表格的屬性發生變化,表格會重新渲染,這時候如果表格數據量不大,沒有復雜處理邏輯還好,但如果表格有性能問題,就會導致整個頁面的體驗變得很差?其實這時候解決方案有很多,我們看一下如何用useState來解決呢?
- // 將columns改為如下代碼
- const [columns] = useState(() => getColumns());
這時候columns的值在初始化之后就不會再發生變化了。有人提出我也可以這樣寫 useState(getColumns()), 實際這樣寫雖然也可以,但是假如getColumns函數自身存在復雜的計算,那么實際上雖然useState自身只會初始化一次,但是getColumn還是會在每次組件重新渲染的時候被執行。
上面的代碼也可以簡化為
- const [columns] = useState(getColumns);
了解hook比較算法的原理
- const useColumns = (options) => {
- const { isEdit, isDelete } = options;
- return useMemo(() => {
- return [
- {
- title: '標題',
- dataIndex: 'title',
- key: 'title',
- },
- {
- title: '操作',
- dataIndex: 'action',
- key: 'action',
- render() {
- return (
- <>
- {isEdit && <Button>編輯</Button>}
- {isDelete && <Button>刪除</Button>}
- </>
- );
- },
- },
- ];
- }, [options]);
- };
- function App() {
- const columns = useColumns({ isEdit: true, isDelete: false });
- const [count, setCount] = useState(1);
- useEffect(() => {
- console.log('columns變了');
- }, [columns]);
- return (
- <div className="App">
- <div>
- <Button onClick={() => setCount(count + 1)}>修改count:{count}</Button>
- </div>
- <Table columns={columns} dataSource={[]}></Table>
- </div>
- );
- }
如上面的代碼,當我們點擊按鈕修改count的時候,我們期待只有count的值會發生變化,但是實際上columns的值也發生了變化。想了解為什么columns會發生變化,我們先了解一下react比較算法的原理。
react比較算法底層是使用的Object.is來比較傳入的state的.
語法: Object.is(value1, value2);
如下代碼是Object.is比較不同數據類型的數據時的返回值:
- Object.is('foo', 'foo'); // trueObject.is(window, window); // trueObject.is('foo', 'bar'); // falseObject.is([], []); // falsevar foo = { a: 1 };var bar = { a: 1 };Object.is(foo, foo); // trueObject.is(foo, bar); // falseObject.is(null, null); // true// 特例Object.is(0, -0); // falseObject.is(0, +0); // trueObject.is(-0, -0); // trueObject.is(NaN, 0/0); // true
通過上面的代碼可以看到,Object.is對于對象的比較是比較引用地址的,而不是比較值的,所以Object.is([], []), Object.is({},{})的結果都是false。而對于基礎類型來說,大家需要注意的是最末尾的四個特列,這是與===所不同的。
再回到上面代碼的例子中,useColumns將傳入的options作為useMemo的第二個參數,而options是一個對象。當組件的count狀態發生變化的時候,會重新執行整個函數組件,這時候useColumns會被調用然后傳入{ isEdit: true, isDelete: false },這是一個新創建的對象,與上一次渲染所創建的options的內容雖然一致,但是Object.is比較結果依然是false,所以columns的結果會被重新創建返回。
通過二次封裝標準化組件
我們在項目中使用antd作為組件庫,雖然antd可以滿足大部分的開發需要,但是有些地方通過對antd進行二次封裝,不僅可以減少開發代碼量,而且對于頁面的交互起到了標準化作用。
看一下下面這個場景, 在我們開發一個數據表格的時候,一般會用到哪些功能呢?
- 表格可以分頁
- 表格最后一列會有操作按鈕
- 表格頂部會有搜索區域
- 表格頂部可能會有操作按鈕
還有其他等等一系列的功能,這些功能在系統中會大量使用,而且其實現方式基本是一致的,這時候如果能把這些功能集成到一起封裝成一個標準的組件,那么既能減少代碼量,而且也會讓頁面展現上更加統一。
以封裝表格操作列為例,一般用操作列我們會像下面這樣封裝
- const columns = [{ title: '操作', dataIndex: 'action', key: 'action', width: '10%', align: 'center', render: (_, row) => { return ( <> <Button type="link" onClick={() => handleEdit(row)}> 編輯 </Button> <Popconfirm title="確認要刪除?" onConfirm={() => handleDelete(row)}> <Button type="link">刪除</Button> </Popconfirm> </> ); } }]
我們期望的是操作列也可以像表格的columns一樣通過配置來生成,而不是寫jsx。看一下如何封裝呢?
- // 定義操作按鈕export interface IAction extends Omit<ButtonProps, 'onClick'> { // 自定義按鈕渲染 render?: (row: any, index: number) => React.ReactNode; onClick?: (row: any, index: number) => void; // 是否有確認提示 confirm?: boolean; // 提示文字 confirmText?: boolean; // 按鈕顯示文字 text: string;}// 定義表格列export interface IColumn<T = any> extends ColumnType<T> { actions?: IAction[];}// 然后我們可以定義一個hooks,專門用來修改表格的columns,添加操作列const useActionButtons = ( columns: IColumn[], actions: IAction[] | undefined): IColumn[] => { return useMemo(() => { if (!actions || actions.length === 0) { return columns; } return [ ...columns, { align: 'center', title: '操作', key: '__action', dataIndex: '__action', width: Math.max(120, actions.length * 85), render(value: any, row: any, index: number) { return actions.map((item) => { if (item.render) { return item.render(row, index); } if(item.confirm) { return <Popconfirm title={item.confirmText || '確認要刪除?'} onConfirm={() => item.onClick?.(row, index)}> <Button type="link">{item.text}</Button> </Popconfirm> } return ( <Button {...item} type="link" key={item.text} onClick={() => item.onClick?.(row, index)} > {item.text} </Button> ); }); } } ]; }, [columns, actions, actionFixed]);};// 最后我們對表格再做一個封裝const CustomTable: React.FC<ITableProps> = ({ actions, columns, ...props}) => { const actionColumns = useActionColumns(columns,actions) // 渲染表格}
通過上面的封裝,我們再使用表格的時候,就可以這樣去寫
- const actions: IAction[] = [ { text: '編輯', onClick: handleModifyRecord, }, ];return <CustomTable actions={actions} columns={columns}></CustomTable>
前端進擊者
原文鏈接:https://mp.weixin.qq.com/s/qJl73MkIYz72PDKx2nEjAA