天天都在寫業務代碼中度過,可是呢,常常在寫業務代碼的時候,會感受本身寫的某些代碼有點彆扭,可是又不知道是哪裏彆扭,今天這篇文章我整理了一些在項目中使用的一些小的技巧點。前端
在使用React Hooks
以前,咱們通常複用的都是組件,對組件內部的狀態是沒辦法複用的,而React Hooks
的推出很好的解決了狀態邏輯的複用,而在咱們平常開發中能作到哪些狀態邏輯的複用呢?下面我羅列了幾個當前我在項目中用到的通用狀態複用。react
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>
);
}
複製代碼
咱們用表格的時候,通常都會用到分頁,經過將分頁封裝成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
傳入函數咱們在使用useState
的setState
的時候,大部分時候都會給setState
傳入一個值,但實際上setState
不但能夠傳入普通的數據,並且還能夠傳入一個函數。下面極端代碼分別描述了幾個傳入函數的例子。數組
以下代碼所示,也有有兩個按鈕,一個按鈕會在點擊後延遲三秒而後給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
的值就是1
app
這時候就須要使用到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
進行二次封裝,不只能夠減小開發代碼量,並且對於頁面的交互起到了標準化做用。
看一下下面這個場景, 在咱們開發一個數據表格的時候,通常會用到哪些功能呢?
還有其餘等等一系列的功能,這些功能在系統中會大量使用,並且其實現方式基本是一致的,這時候若是能把這些功能集成到一塊兒封裝成一個標準的組件,那麼既能減小代碼量,並且也會讓頁面展示上更加統一。
以封裝表格操做列爲例,通常用操做列咱們會像下面這樣封裝
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]);
複製代碼