一篇讓我們學(xué)會(huì)React實(shí)踐
每天都在寫業(yè)務(wù)代碼中度過,但是呢,經(jīng)常在寫業(yè)務(wù)代碼的時(shí)候,會(huì)感覺自己寫的某些代碼有點(diǎn)別扭,但是又不知道是哪里別扭,今天這篇文章我整理了一些在項(xiàng)目中使用的一些小的技巧點(diǎn)。
狀態(tài)邏輯復(fù)用
在使用React Hooks之前,我們一般復(fù)用的都是組件,對(duì)組件內(nèi)部的狀態(tài)是沒辦法復(fù)用的,而React Hooks的推出很好的解決了狀態(tài)邏輯的復(fù)用,而在我們?nèi)粘i_發(fā)中能做到哪些狀態(tài)邏輯的復(fù)用呢?下面我羅列了幾個(gè)當(dāng)前我在項(xiàng)目中用到的通用狀態(tài)復(fù)用。
useRequest
為什么要封裝這個(gè)hook呢?在數(shù)據(jù)加載的時(shí)候,有這么幾點(diǎn)是可以提取成共用邏輯的
- loading狀態(tài)復(fù)用
- 異常統(tǒng)一處理
- 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
我們用表格的時(shí)候,一般都會(huì)用到分頁,通過將分頁封裝成hook,一是可以介紹前端代碼量,二是統(tǒng)一了前后端分頁的參數(shù),也是對(duì)后端接口的一個(gè)約束。
- const usePagination = (
- initPage = {
- total: 0,
- current: 1,
- pageSize: 10,
- }
- ) => {
- const [pagination, setPagination] = useState(initPage);
- // 用于接口查詢數(shù)據(jù)時(shí)的請(qǐng)求參數(shù)
- 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,
- // 用于接口查詢數(shù)據(jù)使用
- queryPagination,
- setTotal,
- setCurrent,
- };
- };
除了上面示例的兩個(gè)hook,其實(shí)自定義hook可以無處不在,只要有公共的邏輯可以被復(fù)用,都可以被定義為獨(dú)立的hook,然后在多個(gè)頁面或組件中使用,我們?cè)谑褂胷edux,react-router的時(shí)候,也會(huì)用到它們提供的hook。
在合適場(chǎng)景給useState傳入函數(shù)
我們?cè)谑褂胾seState的setState的時(shí)候,大部分時(shí)候都會(huì)給setState傳入一個(gè)值,但實(shí)際上setState不但可以傳入普通的數(shù)據(jù),而且還可以傳入一個(gè)函數(shù)。下面極端代碼分別描述了幾個(gè)傳入函數(shù)的例子。
下面的代碼3秒后輸出什么?
如下代碼所示,也有有兩個(gè)按鈕,一個(gè)按鈕會(huì)在點(diǎn)擊后延遲三秒然后給count + 1, 第二個(gè)按鈕會(huì)在點(diǎn)擊的時(shí)候,直接給count + 1,那么假如我先點(diǎn)擊延遲的按鈕,然后多次點(diǎn)擊不延遲的按鈕,三秒鐘之后,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的函數(shù)式組件會(huì)在自己內(nèi)部的狀態(tài)或外部傳入的props發(fā)生變化時(shí),做重新渲染的動(dòng)作。實(shí)際上這個(gè)重新渲染也就是重新執(zhí)行這個(gè)函數(shù)式組件。
當(dāng)我們點(diǎn)擊延遲按鈕的時(shí)候,因?yàn)閏ount的值需要三秒后才會(huì)改變,這時(shí)候并不會(huì)重新渲染。然后再點(diǎn)擊直接加一按鈕,count值由1變成了2, 需要重新渲染。這里需要注意的是,雖然組件重新渲染了,但是setTimeout是在上一次渲染中被調(diào)用的,這也意味著setTimeout里面的count值是組件第一次渲染的值。
所以即使第二個(gè)按鈕加一多次,三秒之后,setTimeout回調(diào)執(zhí)行的時(shí)候因?yàn)橐玫腸ount的值還是初始化的0, 所以三秒后count + 1的值就是1
如何讓上面的代碼延遲三秒后輸出正確的值?
這時(shí)候就需要使用到setState傳入函數(shù)的方式了,如下代碼:
- 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傳入一個(gè)函數(shù),setCount會(huì)調(diào)用這個(gè)函數(shù),并且將前一個(gè)狀態(tài)值作為參數(shù)傳入到函數(shù)中,這時(shí)候我們就可以在setTimeout里面拿到正確的值了。
還可以在useState初始化的時(shí)候傳入函數(shù)
看下面這個(gè)例子,我們有一個(gè)getColumns函數(shù),會(huì)返回一個(gè)表格的所以列,同時(shí)有一個(gè)count狀態(tài),每一秒加一一次。
- function App() {
- const columns = getColumns();
- const [count, setCount] = useState(0);
- useEffect(() => {
- setInterval(() => {
- setCount((prevCount) => prevCount + 1);
- }, 1000);
- }, []);
- useEffect(() => {
- console.log('columns發(fā)生了變化');
- }, [columns]);
- return (
- <div className="App">
- <div>count: {count}</div>
- <Table columns={columns}></Table>
- </div>
- );
- }
上面的代碼執(zhí)行之后,會(huì)發(fā)現(xiàn)每次count發(fā)生變化的時(shí)候,都會(huì)打印出columns發(fā)生了變化,而columns發(fā)生變化便意味著表格的屬性發(fā)生變化,表格會(huì)重新渲染,這時(shí)候如果表格數(shù)據(jù)量不大,沒有復(fù)雜處理邏輯還好,但如果表格有性能問題,就會(huì)導(dǎo)致整個(gè)頁面的體驗(yàn)變得很差?其實(shí)這時(shí)候解決方案有很多,我們看一下如何用useState來解決呢?
- // 將columns改為如下代碼
- const [columns] = useState(() => getColumns());
這時(shí)候columns的值在初始化之后就不會(huì)再發(fā)生變化了。有人提出我也可以這樣寫 useState(getColumns()), 實(shí)際這樣寫雖然也可以,但是假如getColumns函數(shù)自身存在復(fù)雜的計(jì)算,那么實(shí)際上雖然useState自身只會(huì)初始化一次,但是getColumn還是會(huì)在每次組件重新渲染的時(shí)候被執(zhí)行。
上面的代碼也可以簡(jiǎn)化為
- const [columns] = useState(getColumns);
了解hook比較算法的原理
- const useColumns = (options) => {
- const { isEdit, isDelete } = options;
- return useMemo(() => {
- return [
- {
- title: '標(biāo)題',
- 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>
- );
- }
如上面的代碼,當(dāng)我們點(diǎn)擊按鈕修改count的時(shí)候,我們期待只有count的值會(huì)發(fā)生變化,但是實(shí)際上columns的值也發(fā)生了變化。想了解為什么columns會(huì)發(fā)生變化,我們先了解一下react比較算法的原理。
react比較算法底層是使用的Object.is來比較傳入的state的.
語法: Object.is(value1, value2);
如下代碼是Object.is比較不同數(shù)據(jù)類型的數(shù)據(jù)時(shí)的返回值:
- 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對(duì)于對(duì)象的比較是比較引用地址的,而不是比較值的,所以O(shè)bject.is([], []), Object.is({},{})的結(jié)果都是false。而對(duì)于基礎(chǔ)類型來說,大家需要注意的是最末尾的四個(gè)特列,這是與===所不同的。
再回到上面代碼的例子中,useColumns將傳入的options作為useMemo的第二個(gè)參數(shù),而options是一個(gè)對(duì)象。當(dāng)組件的count狀態(tài)發(fā)生變化的時(shí)候,會(huì)重新執(zhí)行整個(gè)函數(shù)組件,這時(shí)候useColumns會(huì)被調(diào)用然后傳入{ isEdit: true, isDelete: false },這是一個(gè)新創(chuàng)建的對(duì)象,與上一次渲染所創(chuàng)建的options的內(nèi)容雖然一致,但是Object.is比較結(jié)果依然是false,所以columns的結(jié)果會(huì)被重新創(chuàng)建返回。
通過二次封裝標(biāo)準(zhǔn)化組件
我們?cè)陧?xiàng)目中使用antd作為組件庫,雖然antd可以滿足大部分的開發(fā)需要,但是有些地方通過對(duì)antd進(jìn)行二次封裝,不僅可以減少開發(fā)代碼量,而且對(duì)于頁面的交互起到了標(biāo)準(zhǔn)化作用。
看一下下面這個(gè)場(chǎng)景, 在我們開發(fā)一個(gè)數(shù)據(jù)表格的時(shí)候,一般會(huì)用到哪些功能呢?
- 表格可以分頁
- 表格最后一列會(huì)有操作按鈕
- 表格頂部會(huì)有搜索區(qū)域
- 表格頂部可能會(huì)有操作按鈕
還有其他等等一系列的功能,這些功能在系統(tǒng)中會(huì)大量使用,而且其實(shí)現(xiàn)方式基本是一致的,這時(shí)候如果能把這些功能集成到一起封裝成一個(gè)標(biāo)準(zhǔn)的組件,那么既能減少代碼量,而且也會(huì)讓頁面展現(xiàn)上更加統(tǒng)一。
以封裝表格操作列為例,一般用操作列我們會(huì)像下面這樣封裝
- const columns = [{ title: '操作', dataIndex: 'action', key: 'action', width: '10%', align: 'center', render: (_, row) => { return ( <> <Button type="link" onClick={() => handleEdit(row)}> 編輯 </Button> <Popconfirm title="確認(rèn)要?jiǎng)h除?" onConfirm={() => handleDelete(row)}> <Button type="link">刪除</Button> </Popconfirm> </> ); } }]
我們期望的是操作列也可以像表格的columns一樣通過配置來生成,而不是寫jsx??匆幌氯绾畏庋b呢?
- // 定義操作按鈕export interface IAction extends Omit<ButtonProps, 'onClick'> { // 自定義按鈕渲染 render?: (row: any, index: number) => React.ReactNode; onClick?: (row: any, index: number) => void; // 是否有確認(rèn)提示 confirm?: boolean; // 提示文字 confirmText?: boolean; // 按鈕顯示文字 text: string;}// 定義表格列export interface IColumn<T = any> extends ColumnType<T> { actions?: IAction[];}// 然后我們可以定義一個(gè)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 || '確認(rèn)要?jiǎng)h除?'} 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]);};// 最后我們對(duì)表格再做一個(gè)封裝const CustomTable: React.FC<ITableProps> = ({ actions, columns, ...props}) => { const actionColumns = useActionColumns(columns,actions) // 渲染表格}
通過上面的封裝,我們?cè)偈褂帽砀竦臅r(shí)候,就可以這樣去寫
- const actions: IAction[] = [ { text: '編輯', onClick: handleModifyRecord, }, ];return <CustomTable actions={actions} columns={columns}></CustomTable>
本文轉(zhuǎn)載自微信公眾號(hào)「前端有的玩」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系前端有的玩公眾號(hào)。