昨天 React 16.8 正式釋出,Hooks 從提案變成正式 API!這是 React 近年來最重要的更新,認真寫一篇。
為什麼需要 Hooks
Class 元件的三個痛點:
- 狀態邏輯難以複用:HOC 巢狀地獄("包裝地獄"),render props 程式碼難讀
- 生命週期邏輯分散:相關邏輯拆分在 componentDidMount / componentDidUpdate / componentWillUnmount
- this 問題:初學者困惑,需要 bind 或箭頭函式
Hooks 讓函式元件有了狀態和副作用,解決了以上問題。
基礎 Hooks
javascript
import React, { useState, useEffect, useRef } from "react";
function Counter() {
// useState:狀態
const [count, setCount] = useState(0);
const [name, setName] = useState("Alice");
// useEffect:副作用(相當於生命週期)
useEffect(() => {
document.title = `計數: ${count}`;
// 返回清理函式(相當於 componentWillUnmount)
return () => {
document.title = "應用";
};
}, [count]); // 依賴陣列:只有 count 變化時才重新執行
// 第二引數為 []:只在掛載時執行一次(componentDidMount)
useEffect(() => {
console.log("元件掛載");
return () => console.log("元件解除安裝");
}, []);
// useRef:引用(不觸發重渲染)
const inputRef = useRef(null);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount((c) => c + 1)}>+1</button>
<input ref={inputRef} />
</div>
);
}
自定義 Hook:邏輯複用
Hooks 最大的價值在於自定義 Hook——可以提取任何有狀態的邏輯。
javascript
// useLocalStorage:持久化到 localStorage 的狀態
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setStoredValue = (value) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setValue(valueToStore);
localStorage.setItem(key, JSON.stringify(valueToStore));
};
return [value, setStoredValue];
}
// useDebounce:防抖值
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// 使用
function SearchBox() {
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) {
fetch(`/api/search?q=${debouncedQuery}`);
}
}, [debouncedQuery]);
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}
Hooks 規則
必須遵守的兩條規則(eslint-plugin-react-hooks 幫你檢查):
- 只在最頂層呼叫 Hook:不能在迴圈、條件、巢狀函式中呼叫
- 只在 React 函式中呼叫 Hook:函式元件 或 自定義 Hook
javascript
// ❌ 錯誤:條件中呼叫
if (condition) {
const [state, setState] = useState(0); // 破壞 Hook 順序
}
// ❌ 錯誤:普通函式中呼叫
function normalFunction() {
const [state] = useState(0); // 不是 React 函式
}
從 Class 元件遷移
javascript
// Class 元件
class Timer extends React.Component {
state = { seconds: 0 };
componentDidMount() {
this.interval = setInterval(() => {
this.setState((s) => ({ seconds: s.seconds + 1 }));
}, 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
return <div>{this.state.seconds}s</div>;
}
}
// 等價的 Hooks 版本
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds((s) => s + 1);
}, 1000);
return () => clearInterval(interval); // 清理
}, []);
return <div>{seconds}s</div>;
}
團隊遷移建議
- 新元件全用 Hooks,不用再寫 Class
- 老元件不強制遷移,Hooks 和 Class 可以共存
- 裝 eslint-plugin-react-hooks 自動檢查 Hooks 規則
- 不要一次性重構,在修改老元件時順便遷移
這次 Hooks 正式版釋出是個歷史節點,函數語言程式設計在 React 生態算是徹底勝利了。