React Hooks常用場景的使用(小結(jié))
前言
React 在 v16.8 的版本中推出了 React Hooks 新特性。在我看來,使用 React Hooks 相比于從前的類組件有以下幾點(diǎn)好處:
- 代碼可讀性更強(qiáng),原本同一塊功能的代碼邏輯被拆分在了不同的生命周期函數(shù)中,容易使開發(fā)者不利于維護(hù)和迭代,通過 React Hooks 可以將功能代碼聚合,方便閱讀維護(hù);
- 組件樹層級變淺,在原本的代碼中,我們經(jīng)常使用 HOC/render props 等方式來復(fù)用組件的狀態(tài),增強(qiáng)功能等,無疑增加了組件樹層數(shù)及渲染,而在 React Hooks 中,這些功能都可以通過強(qiáng)大的自定義的 Hooks 來實(shí)現(xiàn);
關(guān)于這方面的文章,我們根據(jù)使用場景分別進(jìn)行舉例說明,幫助你認(rèn)識理解并可以熟練運(yùn)用 React Hooks 大部分特性。
博客 github地址為:https://github.com/fengshi123/blog
一、State Hook
1、基礎(chǔ)用法
function State(){ const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ) }
2、更新
更新分為以下兩種方式,即直接更新和函數(shù)式更新,其應(yīng)用場景的區(qū)分點(diǎn)在于:
- 直接更新不依賴于舊 state 的值;
- 函數(shù)式更新依賴于舊 state 的值;
// 直接更新 setState(newCount); // 函數(shù)式更新 setState(prevCount => prevCount - 1);
3、實(shí)現(xiàn)合并
與 class 組件中的 setState 方法不同,useState 不會自動(dòng)合并更新對象,而是直接替換它。我們可以用函數(shù)式的 setState 結(jié)合展開運(yùn)算符來達(dá)到合并更新對象的效果。
setState(prevState => { // 也可以使用 Object.assign return {...prevState, ...updatedValues}; });
4、惰性初始化 state
initialState 參數(shù)只會在組件的初始渲染中起作用,后續(xù)渲染時(shí)會被忽略。其應(yīng)用場景在于:創(chuàng)建初始 state 很昂貴時(shí),例如需要通過復(fù)雜計(jì)算獲得;那么則可以傳入一個(gè)函數(shù),在函數(shù)中計(jì)算并返回初始的 state,此函數(shù)只在初始渲染時(shí)被調(diào)用:
const [state, setState] = useState(() => { const initialState = someExpensiveComputation(props); return initialState; });
5、一些重點(diǎn)
(1)不像 class 中的 this.setState ,Hook 更新 state 變量總是替換它而不是合并它;
(2)推薦使用多個(gè) state 變量,而不是單個(gè) state 變量,因?yàn)?state 的替換邏輯而不是合并邏輯,并且利于后續(xù)的相關(guān) state 邏輯抽離;
(3)調(diào)用 State Hook 的更新函數(shù)并傳入當(dāng)前的 state 時(shí),React 將跳過子組件的渲染及 effect 的執(zhí)行。(React 使用 Object.is 比較算法 來比較 state。)
二、Effect Hook
1、基礎(chǔ)用法
function Effect(){ const [count, setCount] = useState(0); useEffect(() => { console.log(`You clicked ${count} times`); }); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ) }
2、清除操作
為防止內(nèi)存泄漏,清除函數(shù)會在組件卸載前執(zhí)行;如果組件多次渲染(通常如此),則在執(zhí)行下一個(gè) effect 之前,上一個(gè) effect 就已被清除,即先執(zhí)行上一個(gè) effect 中 return 的函數(shù),然后再執(zhí)行本 effect 中非 return 的函數(shù)。
useEffect(() => { const subscription = props.source.subscribe(); return () => { // 清除訂閱 subscription.unsubscribe(); }; });
3、執(zhí)行時(shí)期
與 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 調(diào)度的 effect 不會阻塞瀏覽器更新屏幕,這讓你的應(yīng)用看起來響應(yīng)更快;(componentDidMount 或 componentDidUpdate 會阻塞瀏覽器更新屏幕)
4、性能優(yōu)化
默認(rèn)情況下,React 會每次等待瀏覽器完成畫面渲染之后延遲調(diào)用 effect;但是如果某些特定值在兩次重渲染之間沒有發(fā)生變化,你可以通知 React 跳過對 effect 的調(diào)用,只要傳遞數(shù)組作為 useEffect 的第二個(gè)可選參數(shù)即可:如下所示,如果 count 值兩次渲染之間沒有發(fā)生變化,那么第二次渲染后就會跳過 effect 的調(diào)用;
useEffect(() => { document.title = `You clicked ${count} times`; }, [count]); // 僅在 count 更改時(shí)更新
5、模擬 componentDidMount
如果想只運(yùn)行一次的 effect(僅在組件掛載和卸載時(shí)執(zhí)行),可以傳遞一個(gè)空數(shù)組([ ])作為第二個(gè)參數(shù),如下所示,原理跟第 4 點(diǎn)性能優(yōu)化講述的一樣;
useEffect(() => { ..... }, []);
6、最佳實(shí)踐
要記住 effect 外部的函數(shù)使用了哪些 props 和 state 很難,這也是為什么 通常你會想要在 effect 內(nèi)部 去聲明它所需要的函數(shù)。
// bad,不推薦 function Example({ someProp }) { function doSomething() { console.log(someProp); } useEffect(() => { doSomething(); }, []); // 🔴 這樣不安全(它調(diào)用的 `doSomething` 函數(shù)使用了 `someProp`) } // good,推薦 function Example({ someProp }) { useEffect(() => { function doSomething() { console.log(someProp); } doSomething(); }, [someProp]); // ✅ 安全(我們的 effect 僅用到了 `someProp`) }
如果處于某些原因你無法把一個(gè)函數(shù)移動(dòng)到 effect 內(nèi)部,還有一些其他辦法:
- 你可以嘗試把那個(gè)函數(shù)移動(dòng)到你的組件之外。那樣一來,這個(gè)函數(shù)就肯定不會依賴任何 props 或 state,并且也不用出現(xiàn)在依賴列表中了;
- 萬不得已的情況下,你可以 把函數(shù)加入 effect 的依賴但 把它的定義包裹 進(jìn) useCallback Hook。這就確保了它不隨渲染而改變,除非它自身的依賴發(fā)生了改變;
推薦啟用 eslint-plugin-react-hooks 中的 exhaustive-deps 規(guī)則,此規(guī)則會在添加錯(cuò)誤依賴時(shí)發(fā)出警告并給出修復(fù)建議 ;
// 1、安裝插件 npm i eslint-plugin-react-hooks --save-dev // 2、eslint 配置 { "plugins": [ // ... "react-hooks" ], "rules": { // ... "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn" } }
7、一些重點(diǎn)
(1)可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate和 componentWillUnmount這三個(gè)函數(shù)的組合;
(2)在 React 的 class 組件中,render 函數(shù)是不應(yīng)該有任何副作用的; 一般來說,在這里執(zhí)行操作太早了,我們基本上都希望在 React 更新 DOM 之后才執(zhí)行我們的操作。
三、useContext
用來處理多層級傳遞數(shù)據(jù)的方式,在以前組件樹中,跨層級祖先組件想要給孫子組件傳遞數(shù)據(jù)的時(shí)候,除了一層層 props 往下透傳之外,我們還可以使用 React Context API 來幫我們做這件事。使用例子如下所示
(1)使用 React Context API,在組件外部建立一個(gè) Context
import React from 'react'; const ThemeContext = React.createContext(0); export default ThemeContext;
(2)使用 Context.Provider提供了一個(gè) Context 對象,這個(gè)對象可以被子組件共享
import React, { useState } from 'react'; import ThemeContext from './ThemeContext'; import ContextComponent1 from './ContextComponent1'; function ContextPage () { const [count, setCount] = useState(1); return ( <div className="App"> <ThemeContext.Provider value={count}> <ContextComponent1 /> </ThemeContext.Provider> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); } export default ContextPage;
(3)useContext()鉤子函數(shù)用來引入 Context 對象,并且獲取到它的值
// 子組件,在子組件中使用孫組件 import React from 'react'; import ContextComponent2 from './ContextComponent2'; function ContextComponent () { return ( <ContextComponent2 /> ); } export default ContextComponent; // 孫組件,在孫組件中使用 Context 對象值 import React, { useContext } from 'react'; import ThemeContext from './ThemeContext'; function ContextComponent () { const value = useContext(ThemeContext); return ( <div>useContext:{value}</div> ); } export default ContextComponent;
四、useReducer
1、基礎(chǔ)用法
比 useState 更適用的場景:例如 state 邏輯處理較復(fù)雜且包含多個(gè)子值,或者下一個(gè) state 依賴于之前的 state 等;例子如下所示
import React, { useReducer } from 'react'; interface stateType { count: number } interface actionType { type: string } const initialState = { count: 0 }; const reducer = (state:stateType, action:actionType) => { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; default: throw new Error(); } }; const UseReducer = () => { const [state, dispatch] = useReducer(reducer, initialState); return ( <div className="App"> <div>useReducer Count:{state.count}</div> <button onClick={() => { dispatch({ type: 'decrement' }); }}>useReducer 減少</button> <button onClick={() => { dispatch({ type: 'increment' }); }}>useReducer 增加</button> </div> ); }; export default UseReducer;
2、惰性初始化 state
interface stateType { count: number } interface actionType { type: string, paylod?: number } const initCount =0 const init = (initCount:number)=>{ return {count:initCount} } const reducer = (state:stateType, action:actionType)=>{ switch(action.type){ case 'increment': return {count: state.count + 1} case 'decrement': return {count: state.count - 1} case 'reset': return init(action.paylod || 0) default: throw new Error(); } } const UseReducer = () => { const [state, dispatch] = useReducer(reducer,initCount,init) return ( <div className="App"> <div>useReducer Count:{state.count}</div> <button onClick={()=>{dispatch({type:'decrement'})}}>useReducer 減少</button> <button onClick={()=>{dispatch({type:'increment'})}}>useReducer 增加</button> <button onClick={()=>{dispatch({type:'reset',paylod:10 })}}>useReducer 增加</button> </div> ); } export default UseReducer;
五、Memo
如下所示,當(dāng)父組件重新渲染時(shí),子組件也會重新渲染,即使子組件的 props 和 state 都沒有改變
import React, { memo, useState } from 'react';
// 子組件 const ChildComp = () => { console.log('ChildComp...'); return (<div>ChildComp...</div>); }; // 父組件 const Parent = () => { const [count, setCount] = useState(0); return ( <div className="App"> <div>hello world {count}</div> <div onClick={() => { setCount(count => count + 1); }}>點(diǎn)擊增加</div> <ChildComp/> </div> ); }; export default Parent;
改進(jìn):我們可以使用 memo 包一層,就能解決上面的問題;但是僅僅解決父組件沒有傳參給子組件的情況以及父組件傳簡單類型的參數(shù)給子組件的情況(例如 string、number、boolean等);如果有傳復(fù)雜屬性應(yīng)該使用 useCallback(回調(diào)事件)或者 useMemo(復(fù)雜屬性)
// 子組件 const ChildComp = () => { console.log('ChildComp...'); return (<div>ChildComp...</div>); }; const MemoChildComp = memo(ChildComp);
六、useMemo
假設(shè)以下場景,父組件在調(diào)用子組件時(shí)傳遞 info 對象屬性,點(diǎn)擊父組件按鈕時(shí),發(fā)現(xiàn)控制臺會打印出子組件被渲染的信息。
import React, { memo, useState } from 'react'; // 子組件 const ChildComp = (info:{info:{name: string, age: number}}) => { console.log('ChildComp...'); return (<div>ChildComp...</div>); }; const MemoChildComp = memo(ChildComp); // 父組件 const Parent = () => { const [count, setCount] = useState(0); const [name] = useState('jack'); const [age] = useState(11); const info = { name, age }; return ( <div className="App"> <div>hello world {count}</div> <div onClick={() => { setCount(count => count + 1); }}>點(diǎn)擊增加</div> <MemoChildComp info={info}/> </div> ); }; export default Parent;
分析原因:
- 點(diǎn)擊父組件按鈕,觸發(fā)父組件重新渲染;
- 父組件渲染,const info = { name, age } 一行會重新生成一個(gè)新對象,導(dǎo)致傳遞給子組件的 info 屬性值變化,進(jìn)而導(dǎo)致子組件重新渲染。
解決:
使用 useMemo 將對象屬性包一層,useMemo 有兩個(gè)參數(shù):
- 第一個(gè)參數(shù)是個(gè)函數(shù),返回的對象指向同一個(gè)引用,不會創(chuàng)建新對象;
- 第二個(gè)參數(shù)是個(gè)數(shù)組,只有數(shù)組中的變量改變時(shí),第一個(gè)參數(shù)的函數(shù)才會返回一個(gè)新的對象。
import React, { memo, useMemo, useState } from 'react'; // 子組件 const ChildComp = (info:{info:{name: string, age: number}}) => { console.log('ChildComp...'); return (<div>ChildComp...</div>); }; const MemoChildComp = memo(ChildComp); // 父組件 const Parent = () => { const [count, setCount] = useState(0); const [name] = useState('jack'); const [age] = useState(11); // 使用 useMemo 將對象屬性包一層 const info = useMemo(() => ({ name, age }), [name, age]); return ( <div className="App"> <div>hello world {count}</div> <div onClick={() => { setCount(count => count + 1); }}>點(diǎn)擊增加</div> <MemoChildComp info={info}/> </div> ); }; export default Parent;
七 、useCallback
接著第六章節(jié)的例子,假設(shè)需要將事件傳給子組件,如下所示,當(dāng)點(diǎn)擊父組件按鈕時(shí),發(fā)現(xiàn)控制臺會打印出子組件被渲染的信息,說明子組件又被重新渲染了。
import React, { memo, useMemo, useState } from 'react'; // 子組件 const ChildComp = (props:any) => { console.log('ChildComp...'); return (<div>ChildComp...</div>); }; const MemoChildComp = memo(ChildComp); // 父組件 const Parent = () => { const [count, setCount] = useState(0); const [name] = useState('jack'); const [age] = useState(11); const info = useMemo(() => ({ name, age }), [name, age]); const changeName = () => { console.log('輸出名稱...'); }; return ( <div className="App"> <div>hello world {count}</div> <div onClick={() => { setCount(count => count + 1); }}>點(diǎn)擊增加</div> <MemoChildComp info={info} changeName={changeName}/> </div> ); }; export default Parent;
分析下原因:
- 點(diǎn)擊父組件按鈕,改變了父組件中 count 變量值(父組件的 state 值),進(jìn)而導(dǎo)致父組件重新渲染;
- 父組件重新渲染時(shí),會重新創(chuàng)建 changeName 函數(shù),即傳給子組件的 changeName 屬性發(fā)生了變化,導(dǎo)致子組件渲染;
解決:
修改父組件的 changeName 方法,用 useCallback 鉤子函數(shù)包裹一層, useCallback 參數(shù)與 useMemo 類似
import React, { memo, useCallback, useMemo, useState } from 'react'; // 子組件 const ChildComp = (props:any) => { console.log('ChildComp...'); return (<div>ChildComp...</div>); }; const MemoChildComp = memo(ChildComp); // 父組件 const Parent = () => { const [count, setCount] = useState(0); const [name] = useState('jack'); const [age] = useState(11); const info = useMemo(() => ({ name, age }), [name, age]); const changeName = useCallback(() => { console.log('輸出名稱...'); }, []); return ( <div className="App"> <div>hello world {count}</div> <div onClick={() => { setCount(count => count + 1); }}>點(diǎn)擊增加</div> <MemoChildComp info={info} changeName={changeName}/> </div> ); }; export default Parent;
八、useRef
以下分別介紹 useRef 的兩個(gè)使用場景:
1、指向 dom 元素
如下所示,使用 useRef 創(chuàng)建的變量指向一個(gè) input 元素,并在頁面渲染后使 input 聚焦
import React, { useRef, useEffect } from 'react'; const Page1 = () => { const myRef = useRef<HTMLInputElement>(null); useEffect(() => { myRef?.current?.focus(); }); return ( <div> <span>UseRef:</span> <input ref={myRef} type="text"/> </div> ); }; export default Page1;
2、存放變量
useRef 在 react hook 中的作用, 正如官網(wǎng)說的, 它像一個(gè)變量, 類似于 this , 它就像一個(gè)盒子, 你可以存放任何東西. createRef 每次渲染都會返回一個(gè)新的引用,而 useRef 每次都會返回相同的引用,如下例子所示:
import React, { useRef, useEffect, useState } from 'react'; const Page1 = () => { const myRef2 = useRef(0); const [count, setCount] = useState(0) useEffect(()=>{ myRef2.current = count; }); function handleClick(){ setTimeout(()=>{ console.log(count); // 3 console.log(myRef2.current); // 6 },3000) } return ( <div> <div onClick={()=> setCount(count+1)}>點(diǎn)擊count</div> <div onClick={()=> handleClick()}>查看</div> </div> ); } export default Page1;
九、useImperativeHandle
使用場景:通過 ref 獲取到的是整個(gè) dom 節(jié)點(diǎn),通過 useImperativeHandle 可以控制只暴露一部分方法和屬性,而不是整個(gè) dom 節(jié)點(diǎn)。
十、useLayoutEffect
其函數(shù)簽名與 useEffect 相同,但它會在所有的 DOM 變更之后同步調(diào)用 effect,這里不再舉例。
useLayoutEffect 和平常寫的 Class 組件的 componentDidMount 和 componentDidUpdate 同時(shí)執(zhí)行;
useEffect 會在本次更新完成后,也就是第 1 點(diǎn)的方法執(zhí)行完成后,再開啟一次任務(wù)調(diào)度,在下次任務(wù)調(diào)度中執(zhí)行 useEffect;
總結(jié)
關(guān)于這方面的文章,我們根據(jù)使用場景分別進(jìn)行舉例說明,希望有幫助到你認(rèn)識理解并可以熟練運(yùn)用 React Hooks 大部分特性。
到此這篇關(guān)于React Hooks常用場景的使用(小結(jié))的文章就介紹到這了,更多相關(guān)React Hooks常用場景內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
reactjs學(xué)習(xí)解決unknown at rule @tailwind css
這篇文章主要介紹了reactjs學(xué)習(xí)解決unknown at rule @tailwind css問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02react組件從搭建腳手架到在npm發(fā)布的步驟實(shí)現(xiàn)
這篇文章主要介紹了react組件從搭建腳手架到在npm發(fā)布的步驟實(shí)現(xiàn),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-01-01React Native按鈕Touchable系列組件使用教程示例
這篇文章主要為大家介紹了React Native按鈕Touchable系列組件使用教程示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11詳解使用WebPack搭建React開發(fā)環(huán)境
這篇文章主要介紹了詳解使用WebPack搭建React開發(fā)環(huán)境,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08