React Hook是React16.8.0引入的。使可以在不引入class的情況下,可以使用state和其他React特性。
hooks本質上是一些函數。
1. 為什么引入Hook?
1. hooks中的useEffect可以解決class中各邏輯在生命周期函數中管理混亂的問題。
2.hooks中的自定義Hook使得可以不修改組件的結構的基礎上,靈活的復用組件邏輯。
3.class組件不能很好的壓縮,並且熱重載不穩定。不利於組件優化。使用Hook的函數組件不存在這些問題
2. Hook的規則
1. 只能在函數組件中使用hooks
類組件中無效。
2. 只能在函數最外層使用
不能用於if,for等語句中,也不能用於普通js函數中。因為ReactHook通過調用順序確定對應的state等對應的hook方法。使用語句等會改變順序。
3. 只能在以名字use開頭的自定義Hook中使用
3. useState
接受一個參數作為初始值,參數可以是常量,也可以是一個返回值的函數。
初始值如果是一個函數,在初次渲染執行;如果是一個函數的執行,每次渲染都會執行
// 初始值是一個函數 const [count, setCount] = useState(function init(){......;return ..}) // 初始值是一個函數調用 const [count, setCount] = useState(init())
以數組形式返回兩個值,第一個是狀態值(初次渲染創建變量),一個是改變狀態的函數。
例如:
const [count, setCount] = useState(0); // 0是初始值
修改狀態的函數(如setCount)和class中的setState類似,可以接受兩種參數:
setCount(fn/exp)
1)表達式
可以是常量, 也可以是一個帶state值的表達式
<button onClick={() => setCount(0)}>Reset</button> <button onClick={() => setCount(count+1)}>-</button>
2)函數
<button onClick={() => setCount(prevCount => prevCount + 1)}>-</button>
⚠️如果修改后的狀態不變,重復調用只會刷新一次。
<button onClick={() => setCount(count)}>+</button> <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button> <!-- 每次單擊第一個按鈕會渲染一次,繼續單擊不繼續渲染; 但是如果再單擊第二個按鈕,切換到第一個按鈕,還是會渲染一次-->
模擬getDerivedStateFromProps
在render前進行setState更新
應用:在異步函數中獲取state的值
在setTimeout等異步函數中獲取的狀態值,是調用setTimeout方法時的狀態值,不是執行時的狀態值。
function Example() { const [count, setCount] = useState(0); function handleAlertClick() { // count的取值形成了一個Example的閉包,每次刷新都是一個新的閉包 setTimeout(() => { alert('You clicked on: ' + 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> ); } ReactDOM.render(<Example/>, window.root);
4. useEffect
它相當於是componentDidMount,componentDidUpdate,componentWillUnMount三個生命周期的合成體。
它接受兩個參數, 第一個是一個函數(effect),第二個參數可選,是一個數組(第一個函數中用到的可變數據集合)。
useEffect(() => { document.title = `You clicked ${count} times`; }, [count]); // 僅在 count 更改時更新;每次渲染,第一個函數都相當於一個新生成的函數
1. 執行時機
和生命周期不同的是,componentDidMount,componentDidUpdate是DOM渲染完成,屏幕更新前觸發執行,會阻礙屏幕渲染;useEffect中的effect函數(非變更DOM的操作)在瀏覽器完成布局和繪制后,會延遲執行(異步,但是肯定在下一次渲染前執行),不會阻礙屏幕更新。但是如果是變更DOM的操作,需要同步執行。
1) 如果只在初次加載的時候運行,模擬componentDIdMount
useEffect(() => { //TODO }, []);
2)想要只在更新的時候運行,模擬componentDidUpdata
//使用useRef()存儲的實例變量作為是否是第一次執行的標識 const [count, setCount] = useState(0); const first = useRef(true); useEffect(() => { if (first.current) { first.current = false; } else { console.error(count); document.title = `You clicked ${count} times!`; } }, [count]);
2. 執行副作用
如果副作用需要取消,在傳入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 發生變化時,重新訂閱
3. 性能優化
第二個參數傳入的變量,會作為比較的依據。如果變量值不變,就會跳過本次副作用的執行。
作為參數的值是在組件域(即函數組件對應的函數域)中的可變量(props/state),而且在useEffect中使用。
5. useContext
使函數組件可以具有和Class組件中的contextType屬性(使可以通過this.context訪問值)一樣的功能。
用法和Class中contextType基本一致。接受一個context對象作為參數,返回最近的Provider提供的值。
示例代碼:
const ThemeContext = React.createContext('dark'); function App() { const [theme, setTheme] = useState('dark'); return ( <ThemeContext.Provider value={theme}> <Child /> <button onClick={() => setTheme(preTheme => preTheme === 'dark' ? 'light' : 'dark')}>切換主題</button> </ThemeContext.Provider> ); } function Child() { const value = useContext(ThemeContext); return ( <div>{value}</div> ); }
6.useReducer
可以看做useState的復合版。當應用中需要多個狀態時,一般需要調用多次useState(便於組合邏輯)。隨着應用逐漸擴展,會越來越復雜,此時可以使用useReducer。
const [state, dispatch] = useReducer(reducer, initialArg, init); // 第三個值可以不傳,是一個接受第二個參數,返回一個初始值的函數
示例:
import React, { useReducer } from 'react'; import ReactDOM from 'react-dom'; function App() { function reducer(state, action) { switch(action.type) { case 'add': return state + 1; case 'minuse': return state - 1; case 'reset': return 0; default: throw new Error('error'); } } const [count, dispatch] = useReducer(reducer, 0); return ( <div> <p>You clicked {count} times!</p> <button onClick={() => dispatch({type: 'reset'})}>Reset</button> <button onClick={() => dispatch({type: 'add'})}>+</button> <button onClick={() => dispatch({type: 'minuse'})}>-</button> </div> ); }
模擬forceUpdate()
function Example() { const [count, forceUpdate] = useReducer(x=>x+1, 0); return ( <div> <button onClick={() => forceUpdate()}>Click me</button> </div> ); }
7. useMemo
本質上是memorization技術。用於優化在渲染階段,計算數據的函數。
它接受一個計算函數作為第一參數;第二個是個依賴項數組。
返回一個值,該值是第一個計算函數的返回結果。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
示例:
import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import React, {useState, useMemo, useEffect} from 'react'; function App() { const inputRef = React.createRef(); const [data, setData] = useState([]); useEffect(() => { inputRef.current.value = null; }); return ( <div> <input type='text' ref={inputRef}/> <button onClick={() => setData(data.concat(inputRef.current.value))}>添加</button> <Child data={data}/> </div> ); } function Child(props) { const [filterText, setFilterText] = useState(''); const lists = useMemo(() => props.data.filter(i => i && i.includes(filterText)), [filterText, props]); return( <div> <input onChange={(e) => setFilterText(e.target.value)} /> <ul> { lists.map((i,inx) => <li key={inx}>{i}</li>) } </ul> </div> ); } Child.propTypes = { data: PropTypes.array }; ReactDOM.render(<App/>, window.root);
8. useCallback
和 useMemo基本相同;
不同點在於它返回一個函數,這個和'memorize-one'的功能相同。
const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], );
代碼示例:
// 用useCallback替換上面useMemo的示例 const listFilter = useCallback(() => props.data.filter(i => i && i.includes(filterText)), [filterText, props]); const lists = listFilter(filterText, props);
該方法比useRef的優點是,因為返回一個函數,它更容易抽取邏輯,形成自定義Hook
function MeasureDOM() { const [rect, measureRef] = useClientRec(); return ( <> <h1 ref={measureRef}>Hello, world</h1> {rect && <h2>The above header is {Math.round(rect.height)}px tall</h2>} </> ); } function useClientRec() { const [rect, setRect] = useState(); // 此時ref屬性指向一個回調函數 const ref = useCallback(node => { if (node !== null) { setRect(node.getBoundingClientRect()); } }, []); return [rect, ref]; }
9 .useRef
useRef模擬的是Class組件中的實例屬性。可以在函數組件中很方便的保存任何可變值,不引起渲染。
// initialValue是current屬性的初始值 const refContainer = useRef(initialValue);
使用useRef和React.createRef的區別是:
1)useRef返回一個refContainer對象({current: XXX}),並且重新渲染,訪問的都是同一個對象。
2)React.createRef在函數組件中是每次重新渲染,相當於重新創建了一個對象,每次都是一個新對象。
示例:
// 單擊一次后,inputRef1為1, inputRef2仍然為0 import React, {useState,useEffect, useRef} from 'react'; import ReactDOM from 'react-dom'; function App() { const [count, setCount] = useState(0); const inputRef1 = useRef(null); const inputRef2 = React.createRef(); useEffect(() => { if(count === 1) { inputRef1.current.value = count; inputRef2.current.value = count; } }, [count,inputRef1,inputRef2]); return ( <div> <input ref={inputRef1} /><br/> <input ref={inputRef2} /><br/> inputRef1:{inputRef1.current && inputRef1.current.value || 0}<br/> inputRef2:{inputRef2.current && inputRef2.current.value || 0}<br/> <button onClick={() => setCount(count+1)}>Add</button> </div> ); } ReactDOM.render(<App/>, window.root);
應用: 獲取prevState或者prevProps
// 考慮到通用性,將其提取到自定義Hook中usePrevious function App() { const [count, setCount] = useState(0); const previous = usePrevious(count); return ( <div> <p>previous: {previous}</p> <p>current: {count}</p> <button onClick={() => {setCount(count+1);}}>Add</button> </div> ); } // 自定義Hook-獲取prevState/prevProps function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }); return ref.current; }
10. useImperativeHandle
該方法可以自定義(默認是DOM節點)子組件暴露給父組件的內容。
useImperativeHandle(ref, createHandle, [deps])
該方法基於Refs轉發,需要和React.forwardRef結合使用。
示例:
import React, { useRef,useEffect, useImperativeHandle } from 'react'; import ReactDOM from 'react-dom'; function App() { const inputRef = useRef(null); useEffect(() => { // 在父組件查看獲取到的子組件中傳遞的ref的內容 console.log(inputRef); // {current: {focus: () => {...}}} }); return ( <div> <FancyInputWithRef ref={inputRef} /> </div> ); } // 子組件 function FancyInput(props, ref) { // 自定義ref暴露的內容 useImperativeHandle(ref, () => ({ focus: () => ref.current.focus() }), [ref]); return( <div> <input ref={ref} /> </div> ); } // 轉發ref var FancyInputWithRef = React.forwardRef(FancyInput); ReactDOM.render(<App/>, window.root);
11. useLayoutEffect
和useEffect功能基本相同。
主要區別是它的執行時機和componentDidMount,componentDidUpdate相同,都在瀏覽器繪制之前執行。
建議: 先使用useEffect, 有問題再用useLayoutEffect
12. useDebugValue
用於調試時在自定義Hook中添加標簽。
useDebugValue(date, date => date.toDateString());
示例:
function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null); // ... // 在開發者工具中的這個 Hook 旁邊顯示標簽 // e.g. "FriendStatus: Online" useDebugValue(isOnline ? 'Online' : 'Offline'); return isOnline; }