I’ve always thought of the useEffect as a lifecycle. Until a while ago, when I used useEffect in a project, there was a problem of repeated requests. After reading relevant articles, I got a new understanding of useEffect.
Request data can be divided into two types: the first type only initializes the request data on the page; The second is to request data based on external changes (such as changes in props or state).
The page initializes request data
The second argument to effets is [], which means that there are no dependencies and effects will only be executed on the first render.
function SearchResults() { const [data, setData] = useState({ hits: [] }); useEffect(() => { const fetchData async () => { const result = await axios( 'https://hn.algolia.com/api/v1/search?query=react', ); setData(result.data); } fetchData(); } []); / /...Copy the code
You might want to move getFetchUrl outside of Effects to reuse logic
function SearchResults() { const [data, setData] = useState({ hits: [] }); funcyion fetchData(){ async () => { const result = await axios( 'https://hn.algolia.com/api/v1/search?query=react' ); setData(result.data); } } useEffect(() => { fetchData(); } []);Copy the code
If the second effect parameter is set to [], the fetchData dependency is omitted. Should the function dependency be ignored in this case? Effects should not lie about its dependence.
So we specify effects dependency
function SearchResults() {
const [data, setData] = useState({ hits: [] });
funcyion fetchData(){
async () => {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=react'
);
setData(result.data);
}
}
useEffect(() => {
fetchData();
}, [fetchData]);
Copy the code
At this point, everything looks perfect, but what happens if you add a timer? The following is for testing only!!
: Infinite repeat requests have occurred
function SearchResults() { const [data, setData] = useState({ hits: [] }); const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => {setCount(count + 1); }, 1000); return () => clearInterval(id); }, [count]); funcyion fetchData(){ async () => { const result = await axios( 'https://hn.algolia.com/api/v1/search?query=react' ); setData(result.data); } } useEffect(() => { fetchData(); }, [fetchData]);Copy the code
The conut changes, causing the component to render, and effect is executed once after each render, then updates the CONut state in effect to render and trigger effect again.
You should ask the request data dependency has never changed to what would cause effect to perform the request data operation
A common misconception is that “functions never change”. This is clearly not true. In fact, the functions defined within the component change with each rendering.
When the props or state changes, the component will be re-rendered. Each rendering has its own properties, including functions, so every time the component renders the function it depends on, it will perform the corresponding operation.
We can wrap the function using useCallback hook
function SearchResults() {
const fetchData = useCallback(
async ()=>{
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=react'
);
setData(result.data);
},[])
useEffect(() => {
fetchData();
}, [fetchData]);
}
Copy the code
UseCallback essentially adds a layer of dependency checking. It solves the problem in a different way – we make the function itself change only when needed, rather than removing the dependency on the function.
Request data based on external changes
function SearchResults() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('react');
useEffect(() => {
const fetchData async () => {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query='+query
);
setData(result.data);
}
fetchData();
}, [query]);
// ...
Copy the code
Use useCallback to move getFetchUrl outside of Effects
function SearchResults() { const fetchData = useCallback(async ()=>{ const result = await axios( 'https://hn.algolia.com/api/v1/search?query='+query ); setData(result.data); }, depends on [query]) / / specified useEffect (() = > {fetchData (); }, [fetchData]); }Copy the code
When fetchData uses query, the dependency needs to be defined in useCallback, and the fetchData operation is performed only when the query changes.
A full 
This will be done in terms of manually triggered requests for data retrieval, error handling, load display, and how to implement reusable custom hooks for data retrieval
longhand
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( 'https://hn.algolia.com/api/v1/search?query=redux', ); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); useEffect(() => { const fetchData = async () => { setIsError(false); setIsLoading(true); Const result = await axios(url); setData(result.data); } catch (error) { setIsError(true); } setIsLoading(false); }; fetchData(); }, [url]); return ( <Fragment> <form onSubmit={() => setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`); 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>} {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;Copy the code
Custom hooks
Put everything that belongs to the data capture (except query status for input fields, but including load indicators and error handling) into custom hooks without the component knowing the relevant data logic.
const useDataApi = (initialUrl, initialData) => { const [data, setData] = useState(initialData); const [url, setUrl] = useState(initialUrl); 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 [{ data, isLoading, isError }, setUrl]; }; function App() { const [query, setQuery] = useState('redux'); const [{ data, isLoading, isError }, doFetch] = useDataApi( 'https://hn.algolia.com/api/v1/search?query=redux', { hits: []}); return ( <Fragment> <form onSubmit={event => { doFetch( `http://hn.algolia.com/api/v1/search?query=${query}`, ); 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>} {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;Copy the code
This is demo and this is how you get data using custom hooks. Hooks themselves know nothing about the API. It receives all parameters externally and manages only the necessary states, such as data, load, and error states. It exposes results and actions for the component to use.
However, the useDataApi is not yet decoupled from the state and operation. Next, we use useReducer for further optimization
State and operation are decoupled
The Reducer Hook returns a state object and a function that changes the state object. This function — called a scheduler function — takes an initial value of an action and state object with a type and an optional payload.
import React, { Fragment, useState, useEffect, useReducer, } from 'react'; import axios from 'axios'; Const dataFetchReducer = (state, action) => {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(); }}; 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_FAIL' }); }}; fetchData(); }, [url]); return [state, setUrl]; };Copy the code
All that remains is the URL. Instead of reading all the states directly in effect, it dispatches an action that describes what happened. This decouples our effect from isLoading, isError, and data states. Effect doesn’t care about updating the state anymore, it just tells us what’s going on. All the updated logic is left to the dataFetchReducer: here is the full demo
conclusion
- It is recommended to place functions used only by effect inside effect
- If the function is dependent on effect, it needs to be used where the function is defined
useCallback
Pack a layer and you can use it toouseMemo
To deal with - Don’t blindly ignore dependencies and use them
[]
You can go throughuseReducer
和useCallback
To optimize operations or remove effect dependencies