Full text using free translation, not important I did not translate
In this tutorial, I want to show you how to use state and Effect hooks to get data in React. You will also implement custom hooks to retrieve data, which can be reused anywhere in the application or distributed as standalone node packages on NPM.
If you don’t know what’s new with React, check out the React hooks API. If you want to see the full project code for how to get data using React Hooks, see the Github repository
If you only want to retrieve data using React Hooks, use NPM I use-data-API and follow the documentation. If you use it, don’t forget to give me a star
Note: In the future, React Hooks do not apply to getting data in React. A feature called Suspense will take care of it. The following walkthrough is a good way to learn more about state and Effect hooks in React.
Get data using React hooks
If you’re not familiar with extracting data in React, check out the large amount of data I extracted in the React article. It will walk you through getting data using the React class component, how to reuse the data using the Render Prop component and higher-order components, and how it handles errors and loading.
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;
Copy the code
The App component displays a list of items (hits=Hacker News articles). The state and state update functions come from the hooks of useState. He is responsible for managing the state of our data. The first value in userState is the initial value of data. It’s just a deconstruction assignment.
Here we use Axios to get the data, but of course you can use other open source libraries.
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(async () => {
const result = await axios(
'https://hn.algolia.com/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;
Copy the code
Here we use the effect hook of useEffect to get the data. And use setData in useState to update the component state.
But when you run the code above, you’ll notice a particularly annoying looping problem. Effect Hook fires not only when a component is first loaded, but also every time it is updated. Because we set the component state after we get the data, then trigger the Effect hook. So there’s an endless loop. Apparently, this is a bug! We only want to get the data when the component is first loaded, which is why you can provide an empty array as the second argument to useEffect to avoid touching it when the component is updated. In this case, of course, it fires when the component is loaded.
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(async () => {
const result = await axios(
'https://hn.algolia.com/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;
Copy the code
The second argument is used to define all the variables (in this array) that the hook depends on, and if one of the variables changes, the hook is triggered. If an empty array is passed, it is only run on the first load.
Is not feeling, did shouldComponentUpdate thing
There’s another catch. In this code, we use async/await to get the interface data of the third party’S API. According to the documentation, each async will return a promise: the async function declaration defines an asynchronous function that returns an AsyncFunction object. Asynchronous functions are functions that operate asynchronously through an event loop and return results using implicit promises. However, Effect Hooks should not return anything, or clear anything. That’s why you see this Warning: 07:41:22.910 index.js:1452 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.. ` `
This is why we cannot use async in useEffect. But we can solve it by the following ways:
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(
'https://hn.algolia.com/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;
Copy the code
Use React hooks to retrieve API data. However, if you have hooks for error handling, loading, how to trigger data retrieval from a form, or how to implement reusable data retrieval. Read on.
How to trigger the hook automatically or manually? (How to trigger a hook programmatically/manually?)
So far we have retrieved the interface data from the first time the component is loaded. But how do I tell the API that I’m interested in that topic by entering a field? (How to send data to the interface. This is a bit verbose (and the redux keyword to confuse things), so I’ll just jump right into the code…
.function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
useEffect(() => {
const fetchData = async () => {
const result = await axios(
`http://hn.algolia.com/api/v1/search?query=${query}`);setData(result.data); }; fetchData(); } []);return (
...
);
}
export default App;
Copy the code
I’m going to skip a paragraph here because the text is too detailed.
One thing missing: when you try to enter something in a field, it will no longer trigger the request. Since you provide an empty array as the second argument to useEffect is an empty array, the effect hook does not depend on any variables and therefore only fires when the component is first loaded. So here we want the search to be triggered whenever the query field changes
.function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
useEffect(() => {
const fetchData = async () => {
const result = await axios(
`http://hn.algolia.com/api/v1/search?query=${query}`);setData(result.data);
};
fetchData();
}, [query]);
return (
...
);
}
export default App;
Copy the code
As above, we simply pass query to Effect Hook as the second argument, so that the search is triggered every time the Query changes. However, there is another problem: each query field change triggers the search. How do you provide a button to trigger the request?
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://hn.algolia.com/api/v1/search?query=${search}`);setData(result.data);
};
fetchData();
}, [search]);
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>
);
}
Copy the code
The state of the search is set to the initialization state of the component, which triggers the search when the component is loaded. Similar query and search state can cause confusion. Why not set the actual URL to the state instead of the search state?
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',); 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://hn.algolia.com/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> ); }Copy the code
This is an example of using Effect Hook to retrieve data. You can determine the state of the dependency of the Effect Hook. Once you hit setState or something else, effect Hook will run. But in this case, you only get the data again if your URL changes.
Loading (Loading Indicator with React Hooks)
Let’s add a loading to our program, and we need another 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(
'https://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://hn.algolia.com/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;
Copy the code
The code is relatively simple, so I won’t explain it
Error Handling with React Hooks
How do you do some error handling in Effect Hook? The error is just a state. Once the application has error state, the component needs to render some feedback to the user. When we use async/await, we can use try/catch as follows:
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);
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://hn.algolia.com/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;
Copy the code
It is necessary to reset the Error state every time effect Hook is run. Because the user might want to try again if something goes wrong again.
To put it bluntly, the interface is more user-friendly for user feedback
Fetching Data from React Forms Using React Form Forms to fetch Data
function App() {...return (
<Fragment>
<form onSubmit={event => {
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>} ... </Fragment> ); }Copy the code
To prevent reload in the browser, we add an event.preventdefalut (), and the rest is normal form action
Custom Data Fetching Hook (Custom Data Fetching Hook)
This is essentially the encapsulation of the request
In order to extract custom request hooks, except the query field belonging to the input box, other loading loader and error handler functions should be included. Of course, you need to make sure that all fields required by the App Component are returned in your custom hooks
const useHackerNewsApi = () => {
const [data, setData] = useState({ hits: [] });
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);
try {
const result = await axios(url);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
fetchData();
}, [url]);
return [{ data, isLoading, isError }, setUrl];
}
Copy the code
Now we can continue to use your new hook in the component
function App() {
const [query, setQuery] = useState('redux');
const [{ data, isLoading, isError }, doFetch] = useHackerNewsApi();
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>
...
</Fragment>
);
}
Copy the code
Usually we need an initial state. Simply pass it to the custom hook
import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';
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(); }} > <inputtype="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
As mentioned above, we use custom hook to obtain data. The hook itself knows nothing about API. It accepts all parameters externally, but only manages important fields, such as data, loading, error handler, etc. It performs the request and returns all the data required by the component.
Reducer Hook for Data Fetching
So far, we have used various state hooks to manage data, loading, error handlers, and so on. However, all of these states, through their own state management, belong to the same whole, because the data states they care about are request-specific. As you can see, they are both used in the FETCH function. Another good indication that they are of the same type is that they are called one after the other in functions (e.g., setIsError, setIsLoading). Let’s combine these three states with a Reducer Hook!
A Reducer Hook returns a state object and a function that changes the state object. That function is the Dispatch function: an action with a type and parameters.
In fact, these concepts are nothing like Redux
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, }); . };Copy the code
Reducer Hook Takes the Reducer function and the initial state object as parameters. In our example, the parameters of the initial state of the data, load, and error state have not changed, but they have been aggregated into a state object managed by the Reducer Hook rather than a single state hook.
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]); . };Copy the code
Now, when fetching data, you can use dispathc function to pass parameters to the Reducer. Objects sent using the Dispatch function have the required type attribute and the optional payload attribute. This type tells the Reducer function which state transitions need to be applied, and the Reducer can separately use the payload to extract the new state. After all, we only have three state transitions: initialize the extraction process, notify the results of a successful data extraction, and notify the results of an incorrect data extraction.
In our custom hook, state is returned as before. But because we have a state object instead of an independent state. This way, the person calling the useDataApi custom hook can still access the data, isLoading and isError:
const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false, data: initialData, }); .return [state, setUrl];
};
Copy the code
Finally, there is the realization of our Reducer function. It needs to work on three different state transitions called FETCH_INIT, FETCH_SUCCESS, and FETCH_FAILURE. Each state transition needs to return a new state object. Let’s see how to implement this using the Switch case statement:
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(); }};Copy the code
Now, each action is handled and returns a new state.
In summary, the Reducer Hook ensures that this part of state management is encapsulated with its own logic. In addition, you will never encounter invalid states. For example, you might have accidentally set the isLoading and isError states to true in the past. What should the UI display in this case? Each state transition defined by the Reducer function now results in a valid state object.
Abort Data Fetching in Effect Hook
A common problem with React is setting the component state even if the component has been uninstalled (for example, due to the use of React Router navigation). I’ve written about this issue here before, and it describes how to prevent setting state for components that are not loaded in various scenarios. Let’s see how we can prevent setting state in custom hooks for data extraction:
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]);
return [state, setUrl];
};
Copy the code
Every Effect Hook comes with a cleanup function. This function runs when the component is uninstalled. Cleanup is a function returned by hook. In our example, we use a Boolean named didCancel to identify the component’s state. If the component is unloaded, this flag should be set to true, which prevents setting the component state after the final asynchronous parse data extraction.
Note: Data retrieval is not actually aborted – this can be done using Axios Cancellation – but state transitions are no longer performed for unmounted components. Since Axios Cancellation is not the best API in my opinion, the Boolean Cancellation flag, which prevents setup states, will also do the job.
Study and communication
Pay attention to the public number: [full stack front selection] daily access to good articles recommended. You can also join groups to learn and communicate with them