做者:docoder@毛豆前端前端
Examplenode
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `點擊了 ${count} 次`;
});
return (
<div> <p>點擊了 {count} 次</p> <button onClick={() => setCount(count + 1)}> 點擊 </button> </div>
);
}
複製代碼
React 16.8react
No Breaking Changesios
Class 不會被移除git
Hook 被 React 團隊 指望成爲將來寫 React 的主要方式github
除了不經常使用的 getDerivedStateFromError 和 componentDidCatch 在 hook 中尚未被等價實現(很快會添加),其餘幾乎能夠覆蓋全部使用 Class 的狀況web
不影響對React的理解算法
將來 Redux connect() 和 React Router 也會使用相似 useRedux() 或 useRouter() 的自定義 Hooks,固然如今的 API 用法也是兼容的npm
對靜態類型支持更好,如 TypeScriptredux
性能更好
邏輯重用
擁抱 function, 無 Class
Thinking in Hooks
不能在 Class 中使用
建立本身的 Hooks
Hook 讓咱們根據代碼的做用進行拆分
Hooks 只能在頂層調用
不能在循環,條件語句,嵌套函數中調用
保證其順序性和正確性
保證每次 render 都按一樣的順序執行
在多個 useState 和 useEffect 調用過程當中保證 state 的正確性
React 依賴 Hooks 的調用順序來確保 state 和 useState 的對應
// ------------
// First render
// ------------
useState('Mary') // 1. Initialize the name state variable with 'Mary'
useEffect(persistForm) // 2. Add an effect for persisting the form
useState('Poppins') // 3. Initialize the surname state variable with 'Poppins'
useEffect(updateTitle) // 4. Add an effect for updating the title
// -------------
// Second render
// -------------
useState('Mary') // 1. Read the name state variable (argument is ignored)
useEffect(persistForm) // 2. Replace the effect for persisting the form
useState('Poppins') // 3. Read the surname state variable (argument is ignored)
useEffect(updateTitle) // 4. Replace the effect for updating the title
// ...
複製代碼
linter plugin 會進行驗證
只能在 React functions 中調用, 不能在普通的 JavaScript 函數中調用
linter plugin
eslint-plugin-react-hooks
shouldComponentUpdate
React .memo wrap 一個 function component 會淺比較 props
const Button = React.memo((props) => {
// your component
});
複製代碼
useMemo
getDerivedStateFromProps
function ScrollView({row}) {
let [isScrollingDown, setIsScrollingDown] = useState(false);
let [prevRow, setPrevRow] = useState(null);
if (row !== prevRow) {
// Row changed since last render. Update isScrollingDown.
setIsScrollingDown(prevRow !== null && row > prevRow);
setPrevRow(row);
}
return `Scrolling down: ${isScrollingDown}`;
}
複製代碼
state hook
useState
const [state, setState] = useState(initialState);
複製代碼
多個State,屢次使用useState
數組解構,賦予狀態變量不一樣的名字
在每一次渲染中以相同的順序被調用
initialState 沒必要須是對象,實際上不鼓勵是對象,根據實際數據相關性,進行分組和分離。這樣也更利於以後代碼重構,抽取相關邏輯成一個自定義 Hook
//👎
const [state, setState] = useState({ left: 0, top: 0, width: 100, height: 100 });
//👍改成以下:
const [position, setPosition] = useState({ left: 0, top: 0 });
const [size, setSize] = useState({ width: 100, height: 100 });
//重構,自定義 Hook:
function Box() {
const position = useWindowPosition();
const [size, setSize] = useState({ width: 100, height: 100 });
// ...
}
function useWindowPosition() {
const [position, setPosition] = useState({ left: 0, top: 0 });
useEffect(() => {
// ...
}, []);
return position;
}
複製代碼
state 僅在第一次 render 時被建立,以後只是修改使咱們獲得最新的 state
無需像 useEffect 或 useCallback 那樣指定 依賴列表,由 React 來保證
函數式更新 (Functional updates)
新值是經過以前的值計算而來
setCount 能夠接受一個函數,接受以前的 state, 返回新的 state
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<> Count: {count} <button onClick={() => setCount(initialCount)}>Reset</button> <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button> <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button> </> ); } 複製代碼
state 更新了相同的值,不會進行 render 或 觸發 effect
Object.is() 算法,進行比較
useMemo
forceUpdate ?
避免使用
hack
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);
function handleClick() {
forceUpdate();
}
複製代碼
懶初始化
useState 接受一個函數,返回 initialState,僅當初始 render 時被調用進行初始化,只調用一次
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
複製代碼
effect hook
useEffect
useEffect(
() => {
// side effects (獲取數據、設置訂閱和手動更改 React 組件中的 DOM 等)
return () => { // 可選
// clean up
}
}
,
[state, ...] // 可選,僅當 state (或 ...) 改變時,effect 才從新運行
);
複製代碼
將類組件中的 componentDidMount
,componentDidUpdate
和 componentWillUnmount
統一爲一個 API
在每一次 render 後運行 effects ( 包括第一次 render )
useEffect
擁抱閉包,在函數做用域中,可方便訪問 state
經過返回一個函數來 clean up
添加和清理的邏輯能夠彼此靠近
在下次運行 effect 以前清理上一次 render 中的 effect
清理不在 unmount 調用一次,而是在每次 re-render 後調用
避免在缺失 componentDidUpdate
時會產生的 bugs
//當 friend 的屬性改變時,會產生依舊顯示以前 friend 的在線狀態的bug,尤爲當 unmounting 的時候,因爲 unsubscribe 一個錯誤的 friend id 會產生內存泄露甚至 crash
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
複製代碼
//須要使用 componentDidUpdate
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate(prevProps) {
// Unsubscribe 以前的 friend.id
ChatAPI.unsubscribeFromFriendStatus(
prevProps.friend.id,
this.handleStatusChange
);
// Subscribe 新的 friend.id
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
複製代碼
能夠有選擇的運行 effect,從而避免性能問題
useEffect 的第二個參數
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // 僅當 props.friend.id 改變時 effect 才運行
複製代碼
僅在 mount 和 unmount 才運行 effect
[]
錯誤的指定第二個參數(常常是指過少配置), 會形成得不到最新的 props 或 state 的 bug
把函數放到 useEffect 裏, 這樣更安全
function Example({ someProp }) {
function doSomething() {
console.log(someProp);
}
useEffect(() => {
doSomething();
}, []); // 🔴 This is not safe (it calls `doSomething` which uses `someProp`)
}
複製代碼
function Example({ someProp }) {
useEffect(() => {
function doSomething() {
console.log(someProp);
}
doSomething();
}, [someProp]); // ✅ OK (our effect only uses `someProp`)
}
function Example({ someProp }) {
useEffect(() => {
function doSomething() {
console.log('hello');
}
doSomething();
}, []); // ✅ OK in this example because we don't use *any* values from component scope
}
複製代碼
不能將函數放到 effect 裏
確保函數中沒有引用 props 或 state
純計算函數,將該函數返回結果做爲 effect 的依賴
用 useCallback wrap 函數 (確保函數在依賴不變的狀況下,自己不變),再做爲 effect 依賴
function ProductPage({ productId }) {
// ✅ Wrap with useCallback to avoid change on every render
const fetchProduct = useCallback(() => {
// ... Does something with productId ...
}, [productId]); // ✅ All useCallback dependencies are specified
return <ProductDetails fetchProduct={fetchProduct} />; } function ProductDetails({ fetchProduct }) useEffect(() => { fetchProduct(); }, [fetchProduct]); // ✅ All useEffect dependencies are specified // ... } 複製代碼
effect 依賴變更太頻繁
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // This effect depends on the `count` state
}, 1000);
return () => clearInterval(id);
}, []); // 🔴 Bug: `count` is not specified as a dependency
return <h1>{count}</h1>;
}
複製代碼
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1); // ✅ This doesn't depend on `count` variable outside. The identity of the setCount function is guaranteed to be stable so it’s safe to omit.
}, 1000);
return () => clearInterval(id);
}, []); // ✅ Our effect doesn't use any variables in the component scope
return <h1>{count}</h1>;
}
複製代碼
const BookEntryList = props => {
const [pending, setPending] = useState(0);
const [booksJustSaved, setBooksJustSaved] = useState([]);
useEffect(() => {
const ws = new WebSocket(webSocketAddress("/bookEntryWS"));
ws.onmessage = ({ data }) => {
let packet = JSON.parse(data);
if (packet._messageType == "initial") {
setPending(packet.pending);
} else if (packet._messageType == "bookAdded") {
setPending(pending - 1 || 0);
setBooksJustSaved([packet, ...booksJustSaved]);
} else if (packet._messageType == "pendingBookAdded") {
setPending(+pending + 1 || 0);
} else if (packet._messageType == "bookLookupFailed") {
setPending(pending - 1 || 0);
setBooksJustSaved([
{
_id: "" + new Date(),
title: `Failed lookup for ${packet.isbn}`,
success: false
},
...booksJustSaved
]);
}
};
return () => {
try {
ws.close();
} catch (e) {}
};
}, []);
//...
};
複製代碼
function scanReducer(state, [type, payload]) {
switch (type) {
case "initial":
return { ...state, pending: payload.pending };
case "pendingBookAdded":
return { ...state, pending: state.pending + 1 };
case "bookAdded":
return {
...state,
pending: state.pending - 1,
booksSaved: [payload, ...state.booksSaved]
};
case "bookLookupFailed":
return {
...state,
pending: state.pending - 1,
booksSaved: [
{
_id: "" + new Date(),
title: `Failed lookup for ${payload.isbn}`,
success: false
},
...state.booksSaved
]
};
}
return state;
}
const initialState = { pending: 0, booksSaved: [] };
const BookEntryList = props => {
const [state, dispatch] = useReducer(scanReducer, initialState);
useEffect(() => {
const ws = new WebSocket(webSocketAddress("/bookEntryWS"));
ws.onmessage = ({ data }) => {
let packet = JSON.parse(data);
dispatch([packet._messageType, packet]); // The identity of the dispatch function from useReducer is always stable
};
return () => {
try {
ws.close();
} catch (e) {}
};
}, []);
//...
};
複製代碼
exhaustive-deps
ESLint
eslint-plugin-react-hooks
中的一部分function SearchResults() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('react');
useEffect(() => { // 在 useEffect 裏直接使用 async function 是不被容許的,由於 useEffect function 必需要返回一個清理 function 或 nothing。
let ignore = false;
async function fetchData() { //須要在 useEffect function 裏使用 async function
const result = await axios('https://hn.algolia.com/api/v1/search?query=' + query);
if (!ignore) setData(result.data); //當組件 unmount 時,阻止其設置 state
}
fetchData();
return () => { ignore = true; }
}, [query]); // [query] 阻止形成循環,僅當 query 改變時,effect 才執行
return (
<>
<input value={query} onChange={e => setQuery(e.target.value)} />
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
</>
);
}
複製代碼
關注點分離,屢次使用 useEffect
useEffect 不會阻塞瀏覽器渲染
componentDidMount
或componentDidUpdate
會阻塞other hooks
useContext
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
複製代碼
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter({initialState}) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<> Count: {state.count} <button onClick={() => dispatch({type: 'increment'})}>+</button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> </> ); } 複製代碼
const TodosDispatch = React.createContext(null);
function TodosApp() {
// Note: `dispatch` won't change between re-renders
const [todos, dispatch] = useReducer(todosReducer);
return (
<TodosDispatch.Provider value={dispatch}>
<DeepTree todos={todos} />
</TodosDispatch.Provider>
);
}
複製代碼
function DeepChild(props) {
// If we want to perform an action, we can get dispatch from context.
const dispatch = useContext(TodosDispatch);
function handleClick() {
dispatch({ type: 'add', text: 'hello' });
}
return (
<button onClick={handleClick}>Add todo</button>
);
}
複製代碼
useRef
相似實例變量
function Timer() {
const intervalRef = useRef();
useEffect(() => {
const id = setInterval(() => {
// ...
});
intervalRef.current = id; // current 能夠被賦任何值,相似類中的實例變量
return () => {
clearInterval(intervalRef.current);
};
});
// ...
function handleCancelClick() {
clearInterval(intervalRef.current);
}
// ...
}
複製代碼
獲取 previous state
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>;
}
複製代碼
自定義 Hook,usePrevious
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count); // 使用自定義的 Hook
return <h1>Now: {count}, before: {prevCount}</h1>;
}
// 自定義 Hook
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
複製代碼
在一些異步回掉中,讀取最新 state
// 首先點擊 Show alert, 而後點擊 Click me
function Example() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count); //讀取的是點擊 Show alert 時的 count,不是最新值
}, 3000);
}
return (
<div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> <button onClick={handleAlertClick}> Show alert </button> </div>
);
}
複製代碼
// 用 useRef 修改:
function Example() {
const [count, setCount] = useState(0);
//使用 useRef
const countRef = useRef();
countRef.current = count;
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + countRef.current); //經過 ref 訪問 count,讀取的是最新值
}, 3000);
}
return (
<div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> <button onClick={handleAlertClick}> Show alert </button> </div>
);
}
複製代碼
懶加載
useRef 不像 useState 那樣接收函數
function Image(props) {
// ⚠️ IntersectionObserver is created on every render
const ref = useRef(new IntersectionObserver(onIntersect));
// ...
}
複製代碼
function Image(props) {
const ref = useRef(null);
// ✅ IntersectionObserver is created lazily once
function getObserver() {
let observer = ref.current;
if (observer !== null) {
return observer;
}
let newObserver = new IntersectionObserver(onIntersect);
ref.current = newObserver;
return newObserver;
}
// When you need it, call getObserver()
// ...
}
複製代碼
useCallback(fn, deps)
返回一個帶緩存的 callback
僅當所需依賴 deps (數組) 中元素改變時,才執行,不然返回緩存的值
等價於 useMemo(() => fn, deps)
exhaustive-deps
ESLint
// 使用 Callback Refs, 而不用 useRef,這樣當 ref 改變時,能夠獲知
function MeasureExample() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []); // [] 確保 ref callback 不會改變,re-renders 時,不會被重複調用執行
return (
<> <h1 ref={measuredRef}>Hello, world</h1> <h2>The above header is {Math.round(height)}px tall</h2> </> ); } 複製代碼
// 抽取自定義 Hook
function MeasureExample() {
const [rect, ref] = useClientRect();
return (
<> <h1 ref={ref}>Hello, world</h1> {rect !== null && <h2>The above header is {Math.round(rect.height)}px tall</h2> } </> ); } function useClientRect() { const [rect, setRect] = useState(null); const ref = useCallback(node => { if (node !== null) { setRect(node.getBoundingClientRect()); } }, []); return [rect, ref]; } 複製代碼
useMemo(() => fn, deps)
返回一個帶緩存的值,避免消耗性能的計算重複執行
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
複製代碼
性能優化
function Parent({ a, b }) {
// Only re-rendered if `a` changes:
const child1 = useMemo(() => <Child1 a={a} />, [a]);
// Only re-rendered if `b` changes:
const child2 = useMemo(() => <Child2 b={b} />, [b]);
return (
<> {child1} {child2} </> ) } 複製代碼
useImperativeHandle(ref, createHandle, [deps])
當使用 ref 來給父組件提供實例時,用來提供自定義的方法屬性
儘可能避免使用
應該與 forwardRef 一塊兒使用
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} />; } FancyInput = forwardRef(FancyInput); 複製代碼
function Parent() {
const fancyInputRef = useRef();
return (
<>
<FancyInput ref={fancyInputRef} />
<button onClick={()=>{
fancyInputRef.current.focus()
}}>Focus</button>
</>
)
}
複製代碼