useEffect使用指南


最基本的使用

首先,舉一個簡單的例子:

import React, { useState } from 'react'; function App() { const [data, setData] = useState({ hits: [] }); return ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> ); } export default App; 

App組件顯示了一個項目列表,狀態和狀態更新函數來自與useState這個hooks,通過調用useState,來創建App組件的內部狀態。初始狀態是一個object,其中的hits為一個空數組,目前還沒有請求后端的接口。

為了獲取后端提供的數據,接下來將使用axios來發起請求,同樣也可以使用fetch,這里會使用useEffect來隔離副作用。

import React, { useState, useEffect } from 'react'; import axios from 'axios'; function App() { const [data, setData] = useState({ hits: [] }); useEffect(async () => { const result = await axios( 'http://localhost/api/v1/search?query=redux', ); setData(result.data); }); return ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> ); } export default App; 

在useEffect中,不僅會請求后端的數據,還會通過調用setData來更新本地的狀態,這樣會觸發view的更新。

但是,運行這個程序的時候,會出現無限循環的情況。useEffect在組件mount時執行,但也會在組件更新時執行。因為我們在每次請求數據之后都會設置本地的狀態,所以組件會更新,因此useEffect會再次執行,因此出現了無限循環的情況。我們只想在組件mount時請求數據。我們可以傳遞一個空數組作為useEffect的第二個參數,這樣就能避免在組件更新執行useEffect,只會在組件mount時執行。

import React, { useState, useEffect } from 'react'; import axios from 'axios'; function App() { const [data, setData] = useState({ hits: [] }); useEffect(async () => { const result = await axios( 'http://localhost/api/v1/search?query=redux', ); setData(result.data); }, []); return ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> ); } export default App; 

useEffect的第二個參數可用於定義其依賴的所有變量。如果其中一個變量發生變化,則useEffect會再次運行。如果包含變量的數組為空,則在更新組件時useEffect不會再執行,因為它不會監聽任何變量的變更。

還有最后一個問題。在代碼中,我們使用async / await從第三方API獲取數據。如果你對async/await熟悉的話,你會知道,每個async函數都會默認返回一個隱式的promise。但是,useEffect不應該返回任何內容。這就是為什么會在控制台日志中看到以下警告:

Warning: useEffect function must return a cleanup function or nothing. Promises and useEffect(async () => …) are not supported, but you can call an async function inside an effect

這就是為什么不能直接在useEffect中使用async函數,因此,我們可以不直接調用async函數,而是像下面這樣:

import React, { useState, useEffect } from 'react'; import axios from 'axios'; function App() { const [data, setData] = useState({ hits: [] }); useEffect(() => { const fetchData = async () => { const result = await axios( 'http://localhost/api/v1/search?query=redux', ); setData(result.data); }; fetchData(); }, []); return ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> ); } export default App; 

響應更新

上面的例子中,我們實現了再組件mount時請求數據。但是很多情況下,我們需要響應用戶的輸入,然后再請求。這個時候我們會引入一個input框,監聽query值的變化:

import axios from 'axios'; function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState('redux'); useEffect(() => { const fetchData = async () => { const result = await axios( 'http://localhost/api/v1/search?query=redux', ); setData(result.data); }; fetchData(); }, []); return ( <Fragment> <input type="text" value={query} onChange={event => setQuery(event.target.value)} /> <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> </Fragment> ); } export default App; 

有個query值,已經更新query的邏輯,還需要將這個query值傳遞給后台,這個操作會在useEffect中進行:

function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState('redux'); useEffect(() => { const fetchData = async () => { const result = await axios( `http://localhost/api/v1/search?query=${query}`, ); setData(result.data); }; fetchData(); }, []); return ( ... ); } export default App; 

前面我們說了,目前的useEffect只會在組件mount時執行,並且useEffect的第二個參數是依賴的變量,一旦這個依賴的變量變動,useEffect就會重新執行,所以我們需要添加query為useEffect的依賴:

function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState('redux'); useEffect(() => { const fetchData = async () => { const result = await axios( `http://localhost/api/v1/search?query=${query}`, ); setData(result.data); }; fetchData(); }, [query]); return ( ... ); } export default App; 

一旦更改了query值,就可以重新獲取數據。但這會帶來另一個問題:query的任何一次變動都會請求后端,這樣會帶來比較大的訪問壓力。這個時候我們需要引入一個按鈕,點擊這個按鈕再發起請求

function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState('redux'); const [search, setSearch] = useState(''); useEffect(() => { const fetchData = async () => { const result = await axios( `http://localhost/api/v1/search?query=${query}`, ); setData(result.data); }; fetchData(); }, [query]); return ( <Fragment> <input type="text" value={query} onChange={event => setQuery(event.target.value)} /> <button type="button" onClick={() => setSearch(query)}> Search </button> <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> </Fragment> ); } 

可以看到上面我們添加了一個新的按鈕,然后創建新的組件state:search。每次點擊按鈕時,會把search的值設置為query,這個時候我們需要修改useEffect中的依賴項為search,這樣每次點擊按鈕,search值變更,useEffect就會重新執行,避免不必要的變更:

function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState('redux'); const [search, setSearch] = useState('redux'); useEffect(() => { const fetchData = async () => { const result = await axios( `http://localhost/api/v1/search?query=${search}`, ); setData(result.data); }; fetchData(); }, [search]); return ( ... ); } export default App; 

此外,search state的初始狀態設置為與query state 相同的狀態,因為組件首先會在mount時獲取數據。所以簡單點,直接將的要請求的后端URL設置為search state的初始值。

function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState('redux'); const [url, setUrl] = useState( 'http://localhost/api/v1/search?query=redux', ); useEffect(() => { const fetchData = async () => { const result = await axios(url); setData(result.data); }; fetchData(); }, [url]); return ( <Fragment> <input type="text" value={query} onChange={event => setQuery(event.target.value)} /> <button type="button" onClick={() => setUrl(`http://localhost/api/v1/search?query=${query}`) } > Search </button> <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> </Fragment> ); } 

 

如何處理Loading和Error

良好的用戶體驗是需要在請求后端數據,數據還沒有返回時展現loading的狀態,因此,我們還需要添加一個loading的state

import React, { Fragment, useState, useEffect } from 'react'; import axios from 'axios'; function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState('redux'); const [url, setUrl] = useState( 'http://hn.algolia.com/api/v1/search?query=redux', ); const [isLoading, setIsLoading] = useState(false); useEffect(() => { const fetchData = async () => { setIsLoading(true); const result = await axios(url); setData(result.data); setIsLoading(false); }; fetchData(); }, [url]); return ( <Fragment> <input type="text" value={query} onChange={event => setQuery(event.target.value)} /> <button type="button" onClick={() => setUrl(`http://localhost/api/v1/search?query=${query}`) } > Search </button> {isLoading ? ( <div>Loading ...</div> ) : ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> )} </Fragment> ); } export default App; 

在useEffect中,請求數據前將loading置為true,在請求完成后,將loading置為false。我們可以看到useEffect的依賴數據中並沒有添加loading,這是因為,我們不需要再loading變更時重新調用useEffect。請記住:只有某個變量更新后,需要重新執行useEffect的情況,才需要將該變量添加到useEffect的依賴數組中。

loading處理完成后,還需要處理錯誤,這里的邏輯是一樣的,使用useState來創建一個新的state,然后在useEffect中特定的位置來更新這個state。由於我們使用了async/await,可以使用一個大大的try-catch:

import React, { Fragment, useState, useEffect } from 'react'; import axios from 'axios'; function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState('redux'); const [url, setUrl] = useState( 'http://localhost/api/v1/search?query=redux', ); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); useEffect(() => { const fetchData = async () => { setIsError(false); setIsLoading(true); try { const result = await axios(url); setData(result.data); } catch (error) { setIsError(true); } setIsLoading(false); }; fetchData(); }, [url]); return ( <Fragment> <input type="text" value={query} onChange={event => setQuery(event.target.value)} /> <button type="button" onClick={() => setUrl(`http://localhost/api/v1/search?query=${query}`) } > Search </button> {isError && <div>Something went wrong ...</div>} {isLoading ? ( <div>Loading ...</div> ) : ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> )} </Fragment> ); } export default App; 

每次useEffect執行時,將會重置error;在出現錯誤的時候,將error置為true;在正常請求完成后,將error置為false。

處理表單

通常,我們不僅會用到上面的輸入框和按鈕,更多的時候是一張表單,所以也可以在表單中使用useEffect來處理數據請求,邏輯是相同的:

function App() { ... return ( <Fragment> <form onSubmit={() => setUrl(`http://localhost/api/v1/search?query=${query}`) } > <input type="text" value={query} onChange={event => setQuery(event.target.value)} /> <button type="submit">Search</button> </form> {isError && <div>Something went wrong ...</div>} ... </Fragment> ); } 

上面的例子中,提交表單的時候,會觸發頁面刷新;就像通常的做法那樣,還需要阻止默認事件,來阻止頁面的刷新。

function App() { ... const doFetch = () => { setUrl(`http://localhost/api/v1/search?query=${query}`); }; return ( <Fragment> <form onSubmit={event => { doFetch(); event.preventDefault(); }}> <input type="text" value={query} onChange={event => setQuery(event.target.value)} /> <button type="submit">Search</button> </form> {isError && <div>Something went wrong ...</div>} ... </Fragment> ); } 

自定義hooks

我們可以看到上面的組件,添加了一系列hooks和邏輯之后,已經變得非常的龐大。而hooks的一個非常的優勢,就是能夠很方便的提取自定義的hooks。這個時候,我們就能把上面的一大堆邏輯抽取到一個單獨的hooks中,方便復用和解耦:

function useFetchApi = () => { const [data, setData] = useState({ hits: [] }); const [url, setUrl] = useState( 'http://localhost/api/v1/search?query=redux', ); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); useEffect(() => { const fetchData = async () => { setIsError(false); setIsLoading(true); try { const result = await axios(url); setData(result.data); } catch (error) { setIsError(true); } setIsLoading(false); }; fetchData(); }, [url]); const doFetch = () => { setUrl(`http://localhost/api/v1/search?query=${query}`); }; return { data, isLoading, isError, doFetch }; } 

在自定義的hooks抽離完成后,引入到組件中

function App() { const [query, setQuery] = useState('redux'); const { data, isLoading, isError, doFetch } = useHackerNewsApi(); return ( <Fragment> ... </Fragment> ); } 

然后我們需要在form組件中設定初始的后端URL

const useHackerNewsApi = () => { ... useEffect( ... ); const doFetch = url => { setUrl(url); }; return { data, isLoading, isError, doFetch }; }; function App() { const [query, setQuery] = useState('redux'); const { data, isLoading, isError, doFetch } = useHackerNewsApi(); return ( <Fragment> <form onSubmit={event => { doFetch( `http://localhost/api/v1/search?query=${query}`, ); event.preventDefault(); }} > <input type="text" value={query} onChange={event => setQuery(event.target.value)} /> <button type="submit">Search</button> </form> ... </Fragment> ); } 

使用useReducer整合邏輯

到目前為止,我們已經使用了各種state hooks來管理數據,包括loading、error、data等狀態。但是我們可以看到,這三個有關聯的狀態確是分散的,它們通過分離的useState來創建,為了有關聯的狀態整合到一起,我們需要用到useReducer。

如果你寫過redux,那么將會對useReducer非常的熟悉,可以把它理解為一個輕量額redux。useReducer 返回一個狀態對象和一個可以改變狀態對象的dispatch函數。跟redux類似的,dispatch函數接受action作為參數,action包含type和payload屬性。我們看一個簡單的例子吧:

import React, { Fragment, useState, useEffect, useReducer, } from 'react'; import axios from 'axios'; const dataFetchReducer = (state, action) => { ... }; const useDataApi = (initialUrl, initialData) => { const [url, setUrl] = useState(initialUrl); const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, data: initialData, }); ... }; 

useReducer將reducer函數和初始狀態對象作為參數。在我們的例子中,data,loading和error狀態的初始值與useState創建時一致,但它們已經整合到一個由useReducer創建對象,而不是多個useState創建的狀態。

const dataFetchReducer = (state, action) => { ... }; const useDataApi = (initialUrl, initialData) => { const [url, setUrl] = useState(initialUrl); const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, data: initialData, }); useEffect(() => { const fetchData = async () => { dispatch({ type: 'FETCH_INIT' }); try { const result = await axios(url); dispatch({ type: 'FETCH_SUCCESS', payload: result.data }); } catch (error) { dispatch({ type: 'FETCH_FAILURE' }); } }; fetchData(); }, [url]); ... }; 

在獲取數據時,可以調用dispatch函數,將信息發送給reducer。使用dispatch函數發送的參數為object,具有type屬性和可選payload的屬性。type屬性告訴reducer需要應用哪個狀態轉換,並且reducer可以使用payload來創建新的狀態。在這里,我們只有三個狀態轉換:發起請求,請求成功,請求失敗。

在自定義hooks的末尾,state像以前一樣返回,但是因為我們拿到的是一個狀態對象,而不是以前那種分離的狀態,所以需要將狀態對象解構之后再返回。這樣,調用useDataApi自定義hooks的人仍然可以訪問dataisLoading 和 isError這三個狀態。

const useDataApi = (initialUrl, initialData) => { const [url, setUrl] = useState(initialUrl); const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, data: initialData, }); ... const doFetch = url => { setUrl(url); }; return { ...state, doFetch }; }; 

接下來添加reducer函數的實現。它需要三種不同的狀態轉換FETCH_INITFETCH_SUCCESSFETCH_FAILURE。每個狀態轉換都需要返回一個新的狀態對象。讓我們看看如何使用switch case語句實現它:

  switch (action.type) { case 'FETCH_INIT': return { ...state, isLoading: true, isError: false }; case 'FETCH_SUCCESS': return { ...state, isLoading: false, isError: false, data: action.payload, }; case 'FETCH_FAILURE': return { ...state, isLoading: false, isError: true, }; default: throw new Error(); } }; 

取消數據請求

React中的一種很常見的問題是:如果在組件中發送一個請求,在請求還沒有返回的時候卸載了組件,這個時候還會嘗試設置這個狀態,會報錯。我們需要在hooks中處理這種情況,可以看下是怎樣處理的:

const useDataApi = (initialUrl, initialData) => { const [url, setUrl] = useState(initialUrl); const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, data: initialData, }); useEffect(() => { let didCancel = false; const fetchData = async () => { dispatch({ type: 'FETCH_INIT' }); try { const result = await axios(url); if (!didCancel) { dispatch({ type: 'FETCH_SUCCESS', payload: result.data }); } } catch (error) { if (!didCancel) { dispatch({ type: 'FETCH_FAILURE' }); } } }; fetchData(); return () => { didCancel = true; }; }, [url]); const doFetch = url => { setUrl(url); }; return { ...state, doFetch }; }; 

我們可以看到這里新增了一個didCancel變量,如果這個變量為true,不會再發送dispatch,也不會再執行設置狀態這個動作。這里我們在useEffe的返回函數中將didCancel置為true,在卸載組件時會自動調用這段邏輯。也就避免了再卸載的組件上設置狀態。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM