這篇文章兩個月之前寫的,看了一下官網文件沒啥變化,就發出來了。如果有什麼錯誤,歡迎指出~
前言:一直對這個新特性非常感興趣,終於今天有時間,花了大半天時間,把
Hooks
的官方教程過了一遍,收穫頗多,驚歎這個新特性真 TM 好用,以後開發用這個怕是要起飛了?。
狀態鉤子(State Hook)
const [state, setState] = useState(initialState);
複製程式碼
- 多個
useState
時,React
依賴於每次渲染時鉤子的呼叫順序都是一樣的(存在與每個元件關聯的“儲存單元”的內部列表存放JavaScript物件),從而實現鉤子與狀態的一一對應關係。 setState()
接收新的state
或者一個返回state
的函式(setCount(prevCount => prevCount - 1)}
)。- 不同於類元件中的
setState
,useState
返回的setState
不會自動合併更新物件到舊的state
中(可以使用useReducer
)。 useState
可以接收一個函式返回initialState
,它只會在初次渲染時被呼叫。- 當
setState
中的state
和當前的state
相等(通過Object.is
判斷),將會退出更新。 - 建議將一個狀態根據哪些需要值一起變化拆分為多個狀態變數。
const [rows, setRows] = useState(createRows(props.count)); // `createRows()`每次將會渲染將會被呼叫
複製程式碼
優化一下:
const [rows, setRows] = useState(() => createRows(props.count)); // `createRows()`只會被呼叫一次
複製程式碼
其中的() => createRows(props.count)
會賦值給rows
,這樣就保證了只有在rows
呼叫時,才會建立新的值。
作用鉤子(Effect Hook)
useEffect(didUpdate);
複製程式碼
- 相當於生命週期函式
componentDidMount
,componentDidUpdate
,componentWillUnmount
的組合。 - 可以返回一個函式(
cleanup
)用於清理。 - 每次重新渲染都將會發生
cleanup phase
⏬
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
複製程式碼
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
// ====== 原因在這裡 ======
componentDidUpdate(prevProps) {
// Unsubscribe from the previous friend.id
ChatAPI.unsubscribeFromFriendStatus(
prevProps.friend.id,
this.handleStatusChange
);
// Subscribe to the next friend.id
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
複製程式碼
// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange); // Run first effect
// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange); // Run next effect
// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange); // Run next effect
// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // Clean up last effect
複製程式碼
useEffect(() => {document.title = You clicked ${count} times;}, [count]);
,指定第二個引數(這裡為[count
])變化時才發生cleanup phase
,然後執行effect
;- 上面情況,如果
useEffect
第二個引數為為[]
則表示只執行一次(componentDidMount
中執行effect
,componentWillUnmount
中進行cleanup
),永遠不重新執行。 - 和
componentDidMount
/componentDidUpdate
有區別的地方在於,useEffect
中的函式會在layout
和paint
結束後才被觸發。(可以使用useLayoutEffect
在下一次渲染之前(即 DOM 突變之後)同步觸發) useEffect
雖然被推遲到瀏覽器繪製完成之後,但是肯定在有任何新的呈現之前啟動。因為React
總是在開始更新之前重新整理之前渲染的效果。
其他鉤子
useContext
const context = useContext(Context);
複製程式碼
接受一個上下文物件(由React.createContext
建立),返回當前上下文值(由最近的上下文提供)。
附加鉤子(Additional Hooks)
基本鉤子的變體或用於特定邊緣情況的鉤子。
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
複製程式碼
- 第三個引數
init
為函式,將會這樣呼叫:init(initialArg)
,返回初始值。 - 如果返回
state
和現在的state
一樣,將會在不影響子孫或者觸發效果的情況下退出渲染。
useCallback
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
複製程式碼
傳入一個內聯回撥和一個輸入陣列,返回一個帶有記憶的函式,只有輸入陣列中其中一個值變化才會更改。useCallback(fn, inputs)
等價於 useMemo(() => fn, inputs)
。
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
複製程式碼
傳入一個建立函式和一個輸入陣列,返回一個帶有記憶的值,只有輸入陣列中其中一個值變化才會重新計算。
useRef
const refContainer = useRef(initialValue);
// ...
<input ref={refContainer} />
...
複製程式碼
返回一個可變的ref
物件,可以自動將ref
物件中的current
屬性作為初始值傳遞的引數,保持到元件的整個生命週期。
與在類中使用例項欄位的方式類似,它可以保留任何可變值。
如儲存前一個狀態:
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
});
const prevCount = prevCountRef.current;
return <h1>Now: {count}, before: {prevCount}</h1>;
}
複製程式碼
useImperativeHandle
useImperativeHandle(ref, createHandle, [inputs])
複製程式碼
自定在使用 ref 時,公開給父元件的例項值,必須和forwardRef
一起使用。
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
複製程式碼
<FancyInput ref={fancyInputRef} />
// 呼叫
fancyInputRef.current.focus()
複製程式碼
useLayoutEffect
使用方法和useLayoutEffect
一致,不過它是在 DOM 讀取佈局時同步觸發(相當於componentDidMount
和componentDidUpdate
階段)。(建議儘可能使用useEffect
避免阻塞視覺化更新)
useDebugValue
useDebugValue(value)
複製程式碼
用於在React DevTools
中顯示自定義鉤子的標籤,對於自定義鉤子中用於共享的部分有更大價值。
自定義顯示格式:
useDebugValue(date, date => date.toDateString());
複製程式碼
鉤子(Hooks)規則
1. 只能在頂層呼叫,不能再迴圈、條件語句和巢狀函式中使用。 (原因:[State Hook](#State Hook) 第1條)
正確做法:
useEffect(function persistForm() {
// ? We're not breaking the first rule anymore
if (name !== '') {
localStorage.setItem('formData', name);
}
});
複製程式碼
2. 只能在React
函式元件中被呼叫。(可以通過自定義鉤子函式解決)
可以使用eslint-plugin-react-hooks來強制自動執行這些規則。
自定義鉤子(Hook)
- 以
use
開頭,一種公約。 - 自定鉤子是一種複用狀態邏輯的機制(例如設定訂閱和記住當前值),每次使用,內部所有狀態和作用都是獨立的。
- 自定義鉤子每個狀態獨立的能力源於
useState
和useEffect
是完全獨立的。
測試鉤子(Hook)
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
複製程式碼
import React from 'react';
import ReactDOM from 'react-dom';
import { act } from 'react-dom/test-utils';
import Counter from './Counter';
let container;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
container = null;
});
it('can render and update a counter', () => {
// Test first render and effect
act(() => {
ReactDOM.render(<Counter />, container);
});
const button = container.querySelector('button');
const label = container.querySelector('p');
expect(label.textContent).toBe('You clicked 0 times');
expect(document.title).toBe('You clicked 0 times');
// Test second render and effect
act(() => {
button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(label.textContent).toBe('You clicked 1 times');
expect(document.title).toBe('You clicked 1 times');
});
複製程式碼