React實踐指南

天天都在寫業務代碼中度過,可是呢,常常在寫業務代碼的時候,會感受本身寫的某些代碼有點彆扭,可是又不知道是哪裏彆扭,今天這篇文章我整理了一些在項目中使用的一些小的技巧點。前端

狀態邏輯複用

在使用React Hooks以前,咱們通常複用的都是組件,對組件內部的狀態是沒辦法複用的,而React Hooks的推出很好的解決了狀態邏輯的複用,而在咱們平常開發中能作到哪些狀態邏輯的複用呢?下面我羅列了幾個當前我在項目中用到的通用狀態複用。react

useRequest

爲何要封裝這個hook呢?在數據加載的時候,有這麼幾點是能夠提取成共用邏輯的算法

  1. loading狀態複用
  2. 異常統一處理
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,一是能夠介紹前端代碼量,二是統一了先後端分頁的參數,也是對後端接口的一個約束。redux

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傳入函數

咱們在使用useStatesetState的時候,大部分時候都會給setState傳入一個值,但實際上setState不但能夠傳入普通的數據,並且還能夠傳入一個函數。下面極端代碼分別描述了幾個傳入函數的例子。數組

下面的代碼3秒後輸出什麼?

以下代碼所示,也有有兩個按鈕,一個按鈕會在點擊後延遲三秒而後給count + 1, 第二個按鈕會在點擊的時候,直接給count + 1,那麼假如我先點擊延遲的按鈕,而後屢次點擊不延遲的按鈕,三秒鐘以後,count的值是多少?markdown

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發生變化時,作從新渲染的動做。實際上這個從新渲染也就是從新執行這個函數式組件。antd

當咱們點擊延遲按鈕的時候,由於count的值須要三秒後纔會改變,這時候並不會從新渲染。而後再點擊直接加一按鈕,count值由1變成了2, 須要從新渲染。這裏須要注意的是,雖然組件從新渲染了,可是setTimeout是在上一次渲染中被調用的,這也意味着setTimeout裏面的count值是組件第一次渲染的值。react-router

因此即便第二個按鈕加一屢次,三秒以後,setTimeout回調執行的時候由於引用的count的值仍是初始化的0, 因此三秒後count + 1的值就是1app

如何讓上面的代碼延遲三秒後輸出正確的值?

這時候就須要使用到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');     // true
Object.is(window, window);   // true

Object.is('foo', 'bar');     // false
Object.is([], []);           // false

var foo = { a: 1 };
var bar = { a: 1 };
Object.is(foo, foo);         // true
Object.is(foo, bar);         // false

Object.is(null, null);       // true

// 特例
Object.is(0, -0);            // false
Object.is(0, +0);            // true
Object.is(-0, -0);           // true
Object.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進行二次封裝,不只能夠減小開發代碼量,並且對於頁面的交互起到了標準化做用。

看一下下面這個場景, 在咱們開發一個數據表格的時候,通常會用到哪些功能呢?

  1. 表格能夠分頁
  2. 表格最後一列會有操做按鈕
  3. 表格頂部會有搜索區域
  4. 表格頂部可能會有操做按鈕

還有其餘等等一系列的功能,這些功能在系統中會大量使用,並且其實現方式基本是一致的,這時候若是能把這些功能集成到一塊兒封裝成一個標準的組件,那麼既能減小代碼量,並且也會讓頁面展示上更加統一。

以封裝表格操做列爲例,通常用操做列咱們會像下面這樣封裝

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>
複製代碼

避免重複渲染

重複渲染,包含重複計算,重複發請求等等,這個在開發中很容易遇到。好比某一個頁面代碼的時候,某個接口被調用了兩次,對於這種狀況,咱們仍是須要去儘可能避免的。

先看一下下面幾個示例代碼

示例一
function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setCount(1);
    setTimeout(() => {
      setCount(0);
    }, 1000);
  }, [count]);

  return <div>{count}</div>;
}
複製代碼
示例二
//組件
import React, { useEffect } from 'react';

const Test = () => {
  useEffect(() => {
    console.log('此處發送請求');
  }, []);

  return <div></div>;
};
export default Test;

// 頁面
function App() {
   const [count, setCount] = useState(0);
   useEffect(() => {
     setTimeout(() => {
       setCount(1)
     },0)
   },[])
  return <> <Route exact key="test" path="/" component={() => <Test></Test>} /> </>
}
複製代碼
示例三
function App() {
  const [pageSize, setPageSize] = useState(10);
  const [currentPage, setCurrentPage] = useState(1);
  const [update, setUpdate] = useState(0);
  const [appCode, setAppCode] = useState('');
  useEffect(() => {
    console.log('發送請求');
  }, [pageSize, currentPage, update]);

  // 當 appCode 值發生變化時,修改 update 從而從新請求數據
  useEffect(() => {
    setUpdate(update + 1);
  }, [appCode]);

  return <div></div>;
}
複製代碼

請問,上面三個示例存在什麼問題呢?

第一個:會致使死循環

第二個:進入頁面會發送兩次請求

第三個:進入頁面會發送兩次請求

接下來咱們來逐一分析緣由

分析示例一
useEffect(() => {
    setCount(1);
    setTimeout(() => {
      setCount(0);
    }, 1000);
  }, [count]);
複製代碼

上面代碼爲示例一中的useEffect,能夠看到useEffect監聽的是count的變化,並且裏面有一個setTimeout會每一秒鐘修改一次count的值,而count的變化又會致使useEffect從新被執行,而後就進入了死循環。那麼應該如何解決呢?方法就是useEffect不要去監聽count的變化。即改成

useEffect(() => {
    setCount(1);
    setTimeout(() => {
      setCount(0);
    }, 1000);
  }, []);
複製代碼
分析示例二

示例二關鍵問題在於下面這段代碼

<Route exact key="test" path="/" component={() => <Test></Test>} />
複製代碼

在代碼中,count的值初始化爲1,而後一秒鐘後被修改成0, 這會致使App組件產生兩次渲染,注意上面的代碼component傳入的參數是一個箭頭函數,而兩次渲染會致使初始化兩個箭頭函數,這就致使兩次給Route傳入的component是不同的,從而產生兩次渲染,Test組件也就被渲染了兩次,從而內部 的請求發送了兩次。如何去修改呢?

<Route exact key="test" path="/" component={Test} />
複製代碼
分析示例三

示例三中爲了在appCode發生變化時從新請求數據,而後加了一個update屬性,經過調整這個屬性來觸發useEffect執行,可是問題就在於下面這段代碼

// 當 appCode 值發生變化時,修改 update 從而從新請求數據
  useEffect(() => {
    setUpdate(update + 1);
  }, [appCode]);
複製代碼

初始化頁面的時候,useEffect會被默認執行一遍,因此初始化的時候會發送一個請求,同時上面的useEffect也會被執行,這時候update發生變化了,因此又會致使請求再發送一次,如何調整呢?

其實徹底不須要update,直接在下面代碼監聽appCode就行了

useEffect(() => {
    console.log('發送請求');
  }, [pageSize, currentPage, appCode]);
複製代碼
相關文章
相關標籤/搜索