Original: www.robinwieruch.de/react-hooks…
By Robin Wieruch
In this article, I will show you how to get data using Hooks in React by using state and Effect Hooks. We’ll use the well-known Hacker News API to get top articles from the tech world. You can also implement custom hooks for retrieving data, reuse it anywhere in the application, or publish it on NPM as a standalone dependency package.
If you don’t know anything about this new React feature, check out my other article introduction to React Hooks. If you want to see an example of viewing articles directly, check out this Github repository.
Tip: In future releases, React Hooks do not apply to getting data in React. In its place is a feature called Suspense. Still, the following exercise is a good way to learn about state and effect Hooks.
Get data using React Hooks
If you’re not familiar with getting data in React, read my article: How to Fetch Data in React. The article will explain how to get data using Class Components, how to reuse Render Prop Components and higher-order Components, and how to do error handling and loading states. In this article, I’ll re-implement these functions using React Hooks in function Components.
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 hits from Hacker News Articles. The status and status update functions will be generated through the hooks of useState, which will manage the local state of the HITS list data. The initial state is an empty array, for which no state has been set.
We will use Axios to fetch the data, of course you can use another library or the browser’s native FETCH API, if you haven’t already installed AXIos, you can use NPM install Axios at the command line to do so. Then implement effect Hook for data fetching:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: []}); useEffect(async() = > {const result = await axios(
'http://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
Get the data in the effect hook useEffect via an AXIOS API call, then put the data into the component’s local state via setData, and then process the Promise via async/await.
However, when you run the application, you run into a nasty loop. Because Effect Hook is executed not only when the component is mounted, but also during component updates. Since we reset the state after each data fetch, the component updates and Effect Hook runs again, causing the data to be fetched again and again. We only want to get the data during the component mount phase. This is why you pass an empty array to the second parameter of the Effect Hook to avoid executing the Effect Hook during the component update phase, but still execute it during the mount phase.
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: []}); useEffect(async() = > {const result = await axios(
'http://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
If a variable changes, the effect hook is executed once. If it is an empty array, the hooks will not execute on component update because they are not listening for any of the variables.
One last question, in the code we use async/await to get data provided by a third party API. According to the documentation, each async function returns an implicit promise: “An async function defines an asynchronous function that returns an AsyncFunction object. An asynchronous function is a function that operates through an event loop, using an implicit Promise to return the final result.” .
However, effect Hook should return nothing or a clean up function, which is why you will see the following Warning on the console: 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 it is not allowed to use async directly in useEffect functions. Let’s implement its solution by using asynchronous functions inside Effect.
import ...
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(() => {
const fetchData = async () => {
const result = await axios(
'http://hn.algolia.com/api/v1/search?query=redux',);setData(result.data); }; fetchData(); } []);return. ; }export default App;
Copy the code
This is the little 🌰 that gets the data using React Hooks. However, if you’re interested in error handling, loading state, how to trigger data fetching from forms, and how to reuse data fetching hooks, read on.
How to trigger a hook manually or automatically?
Ok, we got the data once after the component was mounted. But how do we use input fields to tell the API about topics we’re interested in? “Redux” is the default query. But what if you want to query about React? Let’s implement an input element that can get topics other than “Redux.” Therefore, a new state is introduced for the input element.
import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux'); useEffect(() => { ... } []);return (
<Fragment>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<ul>
...
</ul>
</Fragment>
);
}
export default App;
Copy the code
Now, the request data and query parameter states are independent of each other, but we want to couple them to get the topic article specified by the parameter entered in the input box. With the following modification, the component should get the corresponding article according to the query parameters after mounting.
.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
There’s still a part to go: When you type something into the input, you don’t get any data after the mount, because we provided [] as a second parameter. Effect doesn’t depend on any variables, so it only fires during the mount phase, but now it should depend on Query, and whenever Query changes, You should retrieve the data.
.function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
useEffect(() => {
...
}, [query]);
return (
...
);
}
export default App;
Copy the code
Ok, now the input value changes and the data is retrieved. But there’s another problem: Every time a new character is entered, effect triggers a new request. So how do we provide a button to manually trigger a data request?
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [search, setSearch] = useState(' ');
useEffect(() => {
...
}, [query]);
return (
<Fragment>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button type="button" onClick={() => setSearch(query)}>
Search
</button>
<ul>
...
</ul>
</Fragment>
);
}
Copy the code
The triggering of effect now relies on search, rather than query, which varies with the input field. Once the user clicks the button, the new search is set and the Effect hook is triggered manually.
.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 (
...
);
}
export default App;
Copy the code
In addition, the initial state of search is set to the same as that of Query, because the component requests data once during the mount phase, and the results should reflect the search criteria in the input box. However, search and Query are somewhat similar, which can seem confusing. Why not set the actual URL of the request to search State?
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',); useEffect((a)= > {
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 getting data via Effect Hook, where you can decide which state effect depends on. Once this state is set in a click or other effect, the effect will run again. In this case, if the STATE of the URL changes, run the effect again to retrieve the topic article through the API.
Loading in React Hooks
Let’s introduce a Loading state during data Loading. It’s just another state managed by the State hook. Loading State is used to render a Loading state in App.
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://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
Once effect is called to retrieve data, which occurs during component mount or when URL state changes, the load state is set to True. After the request completes, the load status is set to false again.
Error handling in React Hooks
What if you do error handling in React Hooks? Errors are just another state initialized with state Hooks. Once an error status occurs, the application component can display feedback to the user. When using async/await, it is common to use try/catch blocks for error handling. You can do this in Effect:
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);
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
The error state is reset each time the hook is re-run. This is useful because each time a request fails, the user might try again, which resets the error. To see if the code works, you can fill in a useless URL and then check to see if an error message appears.
Use forms for data retrieval
Right now we only have input fields and buttons, but once more input elements are introduced, you may want to wrap them with forms. Forms can also trigger the keyboard’s “Enter” event.
function App() {...return (
<Fragment>
<form
onSubmit={() =>
setUrl(`http://hn.algolia.com/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> ); }Copy the code
But now the browser reloads when it clicks the submit button, because this is the default behavior of the browser when submitting the form. To prevent the default behavior, we can cancel the default behavior with event.preventDefault(). This is the same way you do it in the React Class component.
function App() {... constdoFetch = () => {
setUrl(`http://hn.algolia.com/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> ); }Copy the code
Now, when you click the submit button, the browser will no longer reload. It works just as before, but this time it uses a form instead of a simple input and button combination. You can also press enter on your keyboard.
Custom hook to get data
We can define a custom hook that extracts everything related to the data request except the query state of the input box, as well as the Loading state and error handling. Also make sure to return the variables needed in the component.
const useHackerNewsApi = () => {
const [data, setData] = useState({ hits: [] });
const [url, setUrl] = useState(
'http://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]);
const doFetch = () => {
setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`);
};
return { data, isLoading, isError, doFetch };
}
Copy the code
Now your hooks can be used again in App components:
function App() {
const [query, setQuery] = useState('redux');
const { data, isLoading, isError, doFetch } = useHackerNewsApi();
return (
<Fragment>
...
</Fragment>
);
}
Copy the code
Next, pass the URL to the doFetch function:
const useHackerNewsApi = () => { ... useEffect( ... ) ; constdoFetch = 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://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>
...
</Fragment>
);
}
Copy the code
The initial state can also be generic. Simply pass it to the new 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]);
const doFetch = url => {
setUrl(url);
};
return { data, isLoading, isError, doFetch };
};
function App() {
const [query, setQuery] = useState('redux');
const { data, isLoading, isError, doFetch } = useDataApi(
'http://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
This is how you get data using a custom hook. The hook itself is not aware of the API. It gets parameters from the outside, manages only necessary states such as data, Loading, and error state, and executes the request and returns the data to the component via the hook.
Reducer Hook about data acquisition
So far, we have used state hooks to manage data we retrieved, Loading state, and error state. However, all states have their own state hooks, but they are all connected together and care about the same thing. As you can see, all of them are used in data retrieval functions. They are called one after another (e.g., setIsError, setIsLoading), which is the correct way to link them together. Let’s connect the three together using a Reducer Hook.
The Reducer Hook returns a state object and a function that changes the state object. This function is called the dispatch function, and it dispatches an action that has two attributes, type and payload. All this information is received in the Reducer function, which extracts a new state based on the previous state. Let’s see how this works in code:
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 as parameters. In our example, the parameters of the initial state of data, Loading, and error state have not changed, but they have been aggregated into a reducer hook instead of 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 the Dispatch function to send information to the Reducer function. Objects sent using the dispatch function have a mandatory type attribute and an optional payload attribute. The type attribute tells the Reducer function which state to convert, and it can also extract new states from payload. There are only three state transitions: the process of initializing the data, the result of notifying the data request for success, and the result of notifying the data request for failure.
At the end of the custom hook, state is returned as before, but because all of our states are in one object rather than separate states, the state object is deconstructed and returned. This way, people who call useDataApi custom hooks can still data, isLoading, and isError:
const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false, data: initialData, }); . constdoFetch = url => {
setUrl(url);
};
return { ...state, doFetch };
};
Copy the code
Finally, we still lack the reducer function implementation. It needs to handle 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 };
case 'FETCH_SUCCESS':
return { ...state };
case 'FETCH_FAILURE':
return{... state }; default: throw new Error(); }};Copy the code
The Reducer function has access to the current state and incoming actions through its parameters. So far, in the Out Case case statement, each state transition returns only the previous state. Destruct statements are used to keep state objects immutable – meaning that the state never directly mutates – to enforce best practices. Now let’s override some of the properties returned by the current state to change the state of each state transition:
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 state transition (as determined by action.type) returns a new state based on the previous state and payload. For example, payload is used to set the data property of the new state object in the case of a successful request.
In summary, the Reducer hook ensures that it encapsulates this part of state management using its own logic. You always get predictable state changes by providing action Type and optional payload. In addition, invalid states are never encountered. For example, you might have accidentally set isLoading and isError to true in the past. What should be displayed in the UI in this case? Each state transformation defined by the Reducer function now points to a valid state object.
Interrupts a data request in a Hook
A common problem with React is setting the state of a component even after it has been uninstalled (for example, switching routes using the React Router). I’ve previously written about this issue here, which describes how to prevent setState from being called in components that are already unmounted. 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]);
const doFetch = url => {
setUrl(url);
};
return { ...state, doFetch };
};
Copy the code
Each Effect Hook comes with a clean up function that runs when the component is uninstalled. The clean up function is a function returned by the hook. In this case, we use the didCancel variable to let fetchData know the status of the component (mount/unload). If the component is indeed unloaded, the flag should be set to true to prevent the component state from being set after the final asynchronous parsing data is retrieved.
* Note: Data retrieval is not actually aborted (though it can be done via Axios Cancellation), but state transitions are no longer performed for unloaded components. Since Axios cancelling is not the best API in my opinion, the Boolean flag that prevents setting the state would also do the job.