The original link
In this tutorial I want to show you how to use state and effect hooks to request data in React. We’ll use the well-known Hacker News API to get some popular articles. You will define Hooks for your own data requests, which can be reused in all your applications or published to NPM.
If you’re not familiar with these new features of React, check out my other article, Introduction to React Hooks. If you want to see examples of the article directly, you can checkout the Github repository directly.
Note: in future releases of React, Hooks will not use fetching data, instead something called Suspense. Still, the following is a good way to learn about state and effect Hooks.
Use React Hooks for data requests
If you have no experience making data requests in React, read my article How to Fetch Data in React. The article explained how to use Class Components to get data, how to use reusable Render Props components and Higher Order Components, and how to do error handling and loading state. In this article, I want to recreate this using Function Components and React Hooks.
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 information from Hacker News Articles. The state and state update functions are generated through a state hook called useState, which manages the local state of the requested App components. The initial state is an empty array, and there is currently no place to set a new state for it.
We’ll use AXIos to fetch the data, or you can use the request library you’re familiar with, or the browser’s own FETCH API. If you haven’t already installed AXIos, you can do so by using NPM install Axios.
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
In useEffect effect hook, we get data from API through AXIOS and store data to local state using update function of State hook. And use async/await to resolve the promise.
However, when you run the code above, you get stuck in a bloody loop. Effect Hooks are executed when a component is mounted and updated. Because we update state every time we get the data, the component updates and runs Effect again, which requests the data over and over again. Obviously we need to avoid such bugs, we only want to request data when the component is mounted. You can pass an empty array in the second argument provided by Effect Hook to prevent the component from executing the Effect Hook when it is updated, but the component will still execute it when it is mounted.
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
The second argument is used to define the variables that the hook depends on. If one of the variables changes, the hook runs automatically. If the second argument is an empty array, the hook will not run during component updates because it does not monitor any variables.
Another special point to note is that 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:
“The async function declaration defines an asynchronous function, which returns an AsyncFunction object. An asynchronous function is a function which operates asynchronously via the event loop, using an implicit Promise to return its result. “
An async function defines an asynchronous function that returns an asynchronous function object. An asynchronous function is a function that operates through an event loop, using an implicit Promise to return the final result.
However, an Effect hook should return nothing, or a clean up function. This is why you will see an error message on the console.
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.Copy the code
This means that we cannot use async directly in useEffect functions. Let’s implement a solution that uses async functions in Effect Hook.
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://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
This is a small example of a data request using React Hooks. However, if you’re interested in error handling, loading states, how to trigger form data fetching, and how to reuse the issuing processing hooks, let’s move on.
How to trigger a hook manually or automatically?
Now that we can get the data after the component mounts, how do we use the input field to dynamically tell the API to pick a topic of interest? You can see the code before, we will default to “story” as a query parameter (‘ hn.algolia.com/api/v1/sear… React related topics? Let’s implement an input field to get topics other than “Redux”. Now, let’s introduce a new state for the input box.
import React, { Fragment, useState, useEffect } from 'react';
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://hn.algolia.com/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;
Copy the code
Now, the request data and query parameter states are independent of each other, but we need a way to couple them together and only get the topic article specified by the parameter entered in the input box. With the following modification, the component should retrieve the corresponding article from the query after mount.
.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
In fact, we’re missing some code. You’ll notice that when you type in the input field, you don’t get any new data. This is because the second argument to useEffect is just an empty array, and effect does not depend on any variables, so it will only fire once on mount. However, now we need to rely on the query criteria, and once the query sends the change, the data request should be triggered again.
.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
Okay, now as soon as you change the input field, the data will be retrieved. But now there’s another problem: Every time a new character is entered, effect triggers a new request. What if 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('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
In addition, the initial state of search State is set to the same state as query State because the component requests data once during mount, and the result should reflect the search criteria in the input box. However, search state and Query State have similar values, which can seem confusing. Why not set the real URL 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(() = > {
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. In this case, if the STATE of the URL changes, run the effect again to retrieve the topic article through the API.
Loading state and 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 display Loading state in App components.
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
Now when the component is in the mount state or the URL state is changed, effect is called to get the data and the Loading state changes to true. Once the request completes, the Loading state is set to false again.
Error handling with React Hooks
How do you handle errors when using React Hooks for data requests? An error is just another state that uses state Hook initialization. Once an error status occurs, the App component can report back to the user. When using async/await functions, it is common to use try/catch to catch errors. You can do the following in effect:
.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>
...
{isError && <div>Something went wrong ...</div>}... <Fragment> );Copy the code
Effect resets the state of error State each time it is run, which is useful because after each failed request, the user might try again, thus resetting 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
What is the right way to get data? 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() {...const doFetch = (evt) = > {
evt.preventDefault();
setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`);
}
return (
<Fragment>
<form
onSubmit={ doFetch }
>
<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
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 let’s use our new hook in the App component.
function App() {
const [query, setQuery] = useState('redux');
const { data, isLoading, isError, doFetch } = useHackerNewsApi();
return (
<Fragment>.</Fragment>
);
}
Copy the code
Next, pass the URL externally to the DoFetch method.
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://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
The initial state is also generic and can be easily passed to a custom hook with a parameter:
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();
}}
>
<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 how you get data using a custom hook. The Hook itself knows nothing about the API. It gets parameters from the outside, manages only the necessary states, such as data, Loading, and error-related states, and executes the request and returns the data to the component via the hook.
Reducer hooks for 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 a Reducer function and an initial state object as parameters. In our case, the loaded data, Loading state, and error state are all initial state parameters and do not change, but they are aggregated into a state object managed by reducer Hooks instead of a single state hooks.
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, }); .const doFetch = url= > {
setUrl(url);
};
return { ...state, doFetch };
};
Copy the code
Finally, we still lack the reducer function implementation. It handles three different state transitions, called FEATCH_INIT, FEATCH_SUCCESS, and FEATCH_FAILURE. Each state transition needs to return a new state. Let’s see how to implement this logic using switch Case:
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 the actions passed in by Dispatch through its parameters. So far, in switch Case statements, each state transition only returns the previous state, and the destructor statement is used to keep the state object immutable (that is, the state can never be directly changed). Now let’s rewrite some of the properties returned by the current state to change some of the state with each conversion:
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 optional 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.
Interrupt the data request in Effect Hook
A common problem with React is that component state is assigned even after the component has been uninstalled. I wrote about this issue in a previous article, which describes how to prevent setting state for unmounted components in various scenarios. Let’s look at how to prevent setting state when requesting data in a custom hook:
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 by 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.