昨天 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 生態算是徹底勝利了。