React useEffect使用教程
這篇文章會假設(shè)你對useEffectAPI有一定程度的了解。
一、每一次渲染都有它自己的 Props and State
在我們討論 effects 之前,我們需要先討論一下渲染,當我們更新 state 的時候,React會重新渲染組件。每一次渲染都能拿到獨立的 state,這個狀態(tài)值是函數(shù)中的一個常量。
這里關(guān)鍵的點在于任意一次渲染中的常量都不會隨著時間改變。渲染輸出會變是因為我們的組件被一次次調(diào)用,而每一次調(diào)用引起的渲染中,它包含的值獨立于其他渲染。
如果 props 和 state 在不同的渲染中是相互獨立的,那么使用到它們的任何值也是獨立的(包括事件處理函數(shù))。它們都“屬于”一次特定的渲染。即便是事件處理中的異步函數(shù)調(diào)用“看到”的也是這次渲染中的值。
二、每次渲染都有它自己的Effects
讓我們先看向官網(wǎng)的 useEffect 的例子:
function Counter() { const [count, setCount] = useState(0); useEffect(() => { document.title = `You clicked ${count} times`; }); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }
effect是如何讀取到最新的count
狀態(tài)值的呢?
也許,是某種 watching 機制類似 vue 中的數(shù)據(jù)響應式使得能夠在 effect 函數(shù)內(nèi)更新?也或許是一個可變的值,React 會在我們組件內(nèi)部修改它以使我們的 effect 函數(shù)總能拿到最新的值?
都不是。
我們已經(jīng)知道是某個特定渲染中的常量。事件處理函數(shù)“看到”的是屬于它那次特定渲染中的狀態(tài)值。對于 effects 也同樣如此:
并不是count
的值在“不變”的 effect 中發(fā)生了改變,而是 effect 函數(shù)本身在每一次渲染中都不相同。
React 會記住你提供的 effect 函數(shù),并且會在每次更改作用于DOM并讓瀏覽器繪制屏幕后去調(diào)用它。
所以雖然我們說的是一個 effect(這里指更新document的title),但其實每次渲染都是一個不同的函數(shù)— 并且每個 effect 函數(shù)看到的 props 和 state 都來自于它屬于的那次特定渲染。
三、關(guān)于依賴項不要對React撒謊
現(xiàn)在只需要記住:如果你設(shè)置了依賴項,effect 中用到的所有組件內(nèi)的值都要包含在依賴中。這包括props,state,函數(shù)組件內(nèi)的任何東西。
在下面這個組件中,我們的直覺是:“開啟一次定時器,清除也是一次”。直覺上我們會設(shè)置依賴為 '[]'。“我只想運行一次 effect ”,但是這樣對嗎?
function Counter() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(id); }, []); return <h1>{count}</h1>; }
我們以為他會一直遞增下去,但實際上他只會遞增一次,你想要觸發(fā)一次因為它是定時器 ,但為什么會有問題?
在第一次渲染中我們執(zhí)行了setCount(0 + 1)
。但是我們設(shè)置了[]
依賴,effect不會再重新運行,它后面每一秒都會調(diào)用setCount(0 + 1)。
我們對 React 撒謊說我們的 effect 不依賴組件內(nèi)的任何值,可實際上我們的 effect 有依賴。
四、兩種誠實告知依賴的方法
第一種策略是在依賴中包含所有 effect 中用到的組件內(nèi)的值。讓我們在依賴中包含:count
useEffect(() => { const id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(id); }, [count]);
這在我們大部分初級開發(fā)者的眼中都沒有什么問題,并且程序確實不會出任何 bug,現(xiàn)在,每次修改都會重新運行 effect,這能解決問題但是我們的定時器會在每一次改變后清除和重新設(shè)定。這肯定不是我們想要的結(jié)果。
第二種策略是修改 effect 內(nèi)部的代碼以確保它包含的值只會在需要的時候發(fā)生變更。
在這個場景中,我們其實并不需要在effect中使用 count。當我們想要根據(jù)前一個狀態(tài)更新狀態(tài)的時候,我們可以使用的函數(shù)形式:
useEffect(() => { const id = setInterval(() => { setCount(c => c + 1); }, 1000); return () => clearInterval(id); }, []);
我們需要告知React的僅僅是去遞增狀態(tài) - 不管它現(xiàn)在具體是什么值。注意我們做到了移除依賴,并且沒有撒謊。我們的 effect 不再讀取渲染中的count
值。
五、來自useReducer的助攻
如果我們有兩個互相依賴的狀態(tài),或者我們想基于一個 prop 來計算下一次的 state,setCount(c => c + 1)
它并不能做到。幸運的是,有一個更強大的姐妹模式,它的名字叫useReducer。
我們先來修改上面的例子讓它包含兩個狀態(tài):count 和 step 。我們的定時器會每次在 count 上增加一個 step 值:
function Counter() { const [count, setCount] = useState(0); const [step, setStep] = useState(1); useEffect(() => { const id = setInterval(() => { setCount(c => c + step); }, 1000); return () => clearInterval(id); }, [step]); return ( <> <h1>{count}</h1> <input value={step} onChange={e => setStep(Number(e.target.value))} /> </> ); }
注意我們沒有撒謊。既然我們在 effect 里使用了step
,我們就把它加到依賴里。所以這也是為什么代碼能運行正確。
這個例子目前的行為是修改會重啟定時器 - 因為它是依賴項之一。在大多數(shù)場景下,這正是你所需要的。清除上一次的effect然后重新運行新的effect并沒有任何錯。不過,假如我們不想在改變后重啟定時器,我們該如何從effect中移除對的依賴呢?
下面這句話我希望你作為一名 react 開發(fā)人員要記下來:
當你想更新一個狀態(tài),并且這個狀態(tài)更新依賴于另一個狀態(tài)的值時,你可能需要用useReducer
去替換它們。
reducer 可以讓你把組件內(nèi)發(fā)生了什么(actions)和狀態(tài)如何響應并更新分開表述。
我們用一個dispatch
依賴去替換 effect 的依賴 step
:
function Counter() { const [state, dispatch] = useReducer(reducer, initialState); const { count, step } = state; useEffect(() => { const id = setInterval(() => { dispatch({ type: 'tick' }); }, 1000); return () => clearInterval(id); }, [dispatch]); return ( <> <h1>{count}</h1> <input value={step} onChange={e => { dispatch({ type: 'step', step: Number(e.target.value) }); }} /> </> ); } const initialState = { count: 0, step: 1, }; function reducer(state, action) { const { count, step } = state; if (action.type === 'tick') { return { count: count + step, step }; } else if (action.type === 'step') { return { count, step: action.step }; } else { throw new Error(); } }
你可能會問:“這怎么就更好了?”答案是React會保證dispatch
在組件的聲明周期內(nèi)保持不變。所以上面例子中不再需要重新訂閱定時器。
相比于直接在 effect 里面讀取狀態(tài),它 dispatch 了一個action來描述發(fā)生了什么。這使得我們的 effect 和狀態(tài)解耦。我們的 effect 不再關(guān)心怎么更新狀態(tài),它只負責告訴我們發(fā)生了什么。更新的邏輯全都交由 reducer 去統(tǒng)一處理。
六、把函數(shù)移到Effects里
一個典型的誤解是認為函數(shù)不應該成為依賴。舉個例子,下面的代碼看上去可以運行正常:
function SearchResults() { const [data, setData] = useState({ hits: [] }); async function fetchData() { const result = await axios( 'https://hn.algolia.com/api/v1/search?query=react', ); setData(result.data); } useEffect(() => { fetchData(); }, []); // ...
需要明確的是,上面的代碼可以正常工作。但這樣做在組件日漸復雜的迭代過程中我們很難確保它在各種情況下還能正常運行。
如果我們在某些函數(shù)內(nèi)使用了某些 state 或者 prop:
function SearchResults() { const [query, setQuery] = useState('react'); // Imagine this function is also long function getFetchUrl() { return 'https://hn.algolia.com/api/v1/search?query=' + query; } // Imagine this function is also long async function fetchData() { const result = await axios(getFetchUrl()); setData(result.data); } useEffect(() => { fetchData(); }, []); // ... }
如果我們忘記去更新使用這些函數(shù)(很可能通過其他函數(shù)調(diào)用)的effects的依賴,我們的effects就不會同步props和state帶來的變更。這當然不是我們想要的。
如果某些函數(shù)僅在effect中調(diào)用,你可以把它們的定義移到effect中:
function SearchResults() { // ... useEffect(() => { // We moved these functions inside! function getFetchUrl() { return 'https://hn.algolia.com/api/v1/search?query=react'; } async function fetchData() { const result = await axios(getFetchUrl()); setData(result.data); } fetchData(); }, []); }
這么做有什么好處呢?我們不再需要去考慮這些“間接依賴”。我們的依賴數(shù)組也不再撒謊:在我們的 effect 中確實沒有再使用組件范圍內(nèi)的任何東西。
如果我們后面修改getFetchUrl
去使用狀態(tài) query
,我們更可能會意識到我們正在effect里面編輯它因此,我們需要把 query
添加到effect的依賴里:
function SearchResults() { const [query, setQuery] = useState('react'); useEffect(() => { function getFetchUrl() { return 'https://hn.algolia.com/api/v1/search?query=' + query; } async function fetchData() { const result = await axios(getFetchUrl()); setData(result.data); } fetchData(); }, [query]); }
七、我不想把可復用的函數(shù)放到Effect里
有時候你可能不想把函數(shù)移入 effect 里。比如,組件內(nèi)有幾個 effect 使用了相同的函數(shù),你不想在每個 effect 里復制黏貼一遍這個邏輯。也或許這個函數(shù)是一個 prop。
在這種情況下你應該忽略對函數(shù)的依賴嗎?這么做是不對的。再次強調(diào),effects不應該對它的依賴撒謊。通常我們還有更好的解決辦法。一個常見的誤解是,“函數(shù)從來不會改變”。但是這篇文章你讀到現(xiàn)在,你知道這顯然不是事實。實際上,在組件內(nèi)定義的函數(shù)每一次渲染都在變。
function SearchResults() { function getFetchUrl(query) { return 'https://hn.algolia.com/api/v1/search?query=' + query; } useEffect(() => { const url = getFetchUrl('react'); // ... Fetch data and do something ... }, []); useEffect(() => { const url = getFetchUrl('redux'); // ... Fetch data and do something ... }, []); }
在這個例子中,你可能不想把getFetchUrl
移到 effects 中,因為你想復用邏輯。
另一方面,如果你對依賴很“誠實”,你可能會掉到陷阱里。我們的兩個 effects 都依賴 getFetchUrl
,而它每次渲染都不同,所以我們的依賴數(shù)組會變得無用:
function SearchResults() { function getFetchUrl(query) { return 'https://hn.algolia.com/api/v1/search?query=' + query; } useEffect(() => { const url = getFetchUrl('react'); // ... Fetch data and do something ... }, [getFetchUrl]); useEffect(() => { const url = getFetchUrl('redux'); // ... Fetch data and do something ... }, [getFetchUrl]); // ... }
我們有兩個更簡單的解決辦法。
第一個, 如果一個函數(shù)沒有使用組件內(nèi)的任何值,你應該把它提到組件外面去定義,然后就可以自由地在 effects 中使用:
function getFetchUrl(query) { return 'https://hn.algolia.com/api/v1/search?query=' + query; } function SearchResults() { useEffect(() => { const url = getFetchUrl('react'); // ... Fetch data and do something ... }, []); useEffect(() => { const url = getFetchUrl('redux'); // ... Fetch data and do something ... }, []); // ... }
你不再需要把它設(shè)為依賴,因為它們不在渲染范圍內(nèi),因此不會被數(shù)據(jù)流影響。
或者, 你也可以把它包裝成useCallback Hook:
function SearchResults() { const getFetchUrl = useCallback((query) => { return 'https://hn.algolia.com/api/v1/search?query=' + query; }, []); useEffect(() => { const url = getFetchUrl('react'); // ... Fetch data and do something ... }, [getFetchUrl]); useEffect(() => { const url = getFetchUrl('redux'); // ... Fetch data and do something ... }, [getFetchUrl]); // ... }
我們用 useCallback 對 getFetchUrl 做了一層緩存,現(xiàn)在只有當依賴項變化的時候,才會重新執(zhí)行 useCallback 來返回新的函數(shù),依賴項沒有變化的時候就算組件 rerender 了,這個函數(shù)也不會重新執(zhí)行,這樣我們把 getFetchUrl 作為 useEffect 的依賴就沒問題了。
不同于傳遞參數(shù)的方式,現(xiàn)在我們從狀態(tài)中讀取 query:
function SearchResults() { const [query, setQuery] = useState('react'); const getFetchUrl = useCallback(() => { return 'https://hn.algolia.com/api/v1/search?query=' + query; }, [query]); useEffect(() => { const url = getFetchUrl(); // ... Fetch data and do something ... }, [getFetchUrl]); // ... }
如果query
保持不變,useCallback
也會保持不變,我們的 effect 也不會重新運行。但是如果修改了 query,useCallback 也會隨之改變,因此會重新請求數(shù)據(jù)。這就像你在Excel里修改了一個單元格的值,另一個使用它的單元格會自動重新計算一樣。
到此這篇關(guān)于React useEffect使用教程的文章就介紹到這了,更多相關(guān)React useEffect內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React Hooks中模擬Vue生命周期函數(shù)的指南
React Hooks 提供了一種在函數(shù)組件中使用狀態(tài)和其他 React 特性的方式,而不需要編寫類組件,Vue 的生命周期函數(shù)和 React Hooks 之間有一定的對應關(guān)系,本文給大家介紹了React Hooks中模擬Vue生命周期函數(shù)的指南,需要的朋友可以參考下2024-10-10React如何使用refresh_token實現(xiàn)無感刷新頁面
本文主要介紹了React如何使用refresh_token實現(xiàn)無感刷新頁面,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-04-04關(guān)于React Native使用axios進行網(wǎng)絡(luò)請求的方法
axios是一個基于Promise的Http網(wǎng)絡(luò)庫,可運行在瀏覽器端和Node.js中,Vue應用的網(wǎng)絡(luò)請求基本都是使用它完成的。這篇文章主要介紹了React Native使用axios進行網(wǎng)絡(luò)請求,需要的朋友可以參考下2021-08-08React?高階組件與Render?Props優(yōu)缺點詳解
這篇文章主要weidajai?介紹了React?高階組件與Render?Props優(yōu)缺點詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-11-11