In this article, I want to show you how to get data from the React Hooks using state and Effecthooks. We’ll use the well-known Hacker News API in the tech world to get popular articles. You can also use your own method to get hooks data, which you can reuse in your application or publish as a Node package in the NPM library.

If you don’t know anything about React’s new features, read the introduction to React Hooks link first. GitHub Repository GitHub Repository GitHub Repository GitHub repository GitHub repository GitHub repository GitHub Repository GitHub Repository

If you’re ready to use React Hook to get your data. Run NPM install use-data-api in your project and view the documentation. If it works, give it a star

Tip: In the future, React Hooks are not used to retrieve data in the React project. Instead, a new feature called Suspense will take its place. However, the following is a good way to learn about states and effects in React.

throughReact HooksTo get the data

If you’re not familiar with getting data in React, check out my previous posts. This article will walk you through getting data in class components. This article will walk you through data reuse through props and advanced components. Resolve errors and progress bar loading handling. In this article, I’ll show you how to implement the above usage in ReactHooks, through the Function component, of course.

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 above APP component shows a list. The list’s status and status updates are made using useState. UseState dynamically manages the data we want to render into the component. The initialization data is an empty property list in the object. Now, the data is empty.

We’ll use Axios to get the data asynchronously. Of course, you can use your own library or native request API to request data from the browser. If you haven’t installed AXIos yet, you can execute NPM I axios in your project to do so. And then you could write it like this.

import React, { useState, useEffect } from 'react'; import axios from 'axios'; 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 method is updated within the component by using the useEffect method to get the data through AXIos and assign the local state. The promise request uses async/await. When you run your application, you should encounter an awkward situation. Effect is executed both when the component is loaded and when it is updated. Because we triggered the data request after the status update, the request went into an infinite loop. This is a bug that needs to be avoided. We only want to request data after the component is loaded. This is why you put an empty array in effect’s second parameter, which avoids triggering effect execution after a state update and only after the component has 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 can be used to define any variable on which the hook depends. If one of the variables changes, effect execution is triggered. If this parameter is an empty array, the update of variables within the component will not be performed. This means that it does not need to listen for any variable updates.

And then one last question. Async /await is used in the code to request data using a third-party API. According to the documentation, functions prefixed with async need to return 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.

However, Effect does not need to return anything or a clean method. That’s why you 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 async cannot be used directly in useEffect. Let’s solve this by using async in useEffect.

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

Above, this is a simple example of getting data using React Hooks. If you’re interested in handling error handling, loading animations, retrieving data from forms, and handling reusable methods of retrieving data, you can read on.

How do I trigger hooks programmatically/manually?

Nice! We’ve got the data after the page loads. But how do you get the text field input? Default is’ Redux ‘query. But what about the response? Let’s create an input element and get the data without using the ‘Redux’ implementation. Now, let’s introduce a new state for input.

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( 'https://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

Currently, these two states are independent of each other, but now you want to couple them so that you only get the items specified by the query in the input field. With this change, you only load data once the component is loaded.

. 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

One thing that gets overlooked is that when you enter content in the text box, the interface is not called after loading the effect hook. The reason for this is that you set the second parameter of the effect hook to null. Effect doesn’t listen for any variables, so it doesn’t fire until the page is fully loaded. However, effect ‘firing now depends on the query. Once the text box is entered, the request continues.

. 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

Requests should only be triggered if your textbox values change. There’s another problem: Effect triggers a data request at the end of each input. What if you want a button to trigger it?

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://hn.algolia.com/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>
  );
}
Copy the code

Now, the effect depends on the search state, rather than the fluctuating query state that varies with each stroke in the input field. Once the user clicks the search button, the search text is assigned a value and effect is manually triggered to perform the search.

. 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

As the value of the search, the initial initialization is the same as the value of state. When the page is loaded, the request for data will be returned with the initial value of the input field. And then it’s a little bit confusing. Can we put parameters in the URL and read them by default?

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 the condition for using the Effect hook to retrieve implicit programming data. You can use Effect to listen for any state variables you need. An effect is triggered after you set it, after you click it, or after you bind a listener. In this example, data will be requested when the URL changes.

Load loading effects in React Hooks

Let’s take a look at implementing the loading effect before the request completes. The effect is to manage an additional state. This effect is achieved by adding an additional loading animation component to the component.

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

When the component loads or the URL state changes, the Effect hook is triggered to load data. All you need to do is set the load component to true. Set the load component to false when the data is fully loaded.

Handle error exceptions in React Hooks

How do you handle error exceptions in ReactHooks? The error message is just another initialization state. The component gives feedback to the user whenever an error message occurs. When processing a request with async/await, it is common to use a try/catch to catch an exception. Look at the code.

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

The error state is reset each time the hook function executes. An error request is generated and you may want to try again. That’s fine. To test for errors, you can set the URL parameter to incorrect, and then verify that the error message is displayed.

Get data from the form

How about getting data from the form? So far we’ve just used a combination of text field input and button clicks to get the data. As soon as you want to add more text fields, you’ll use FormL to handle data fetching. In addition, a form can trigger a data retrieval with a carriage return on the keyboard.

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

When the submit button is clicked, the browser refreshes because the submit form button refreshes by default. To avoid default behavior, we can add a method to the React event. Here’s how to write the class usage.

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

Clicking the submit button will no longer trigger a refresh. Okay, this time we’ve implemented a form to get data instead of just typing in a text box and clicking a button.

Custom data structure to get data

To extract the custom hooks for data retrieval, move everything that belongs to data retrieval (except the query state that belongs to the input field, but includes load indicators and error handling) into its own function. Also ensure that all necessary variables are returned from the functions used in the application component.

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, you can write it this way.

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

Initialization data can also be written this way. Just 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]); 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

You can do this under hooks. The hook itself has nothing to do with the API. It receives all parameters externally and manages only the necessary states, such as data, load, and error states. It executes the request and returns the data to the component as a custom data fetch hook.

Restore hooks for data retrieval

So far, we have used various state hooks to manage the fetch state, load state, and error state of the data. Somehow, however, all of these states, managed with their own state hooks, belong together because they care about the same reason. As you can see, they are both used in data retrieval functions. A good indication that they belong together is that they are used one after another (for example, setIsError, setIsLoading). Let’s replace these three with a reducing hook.

The Reducer hook returns a state object and a function to change the state object. This function (called the Dispatch function) performs an operation with a type and an optional load. All of this information is used in the actual Reducer function to extract the new state from the previous state, the optional load and type of the operation. 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

The Reducer hook takes the Reducer function and the initial state object as parameters. In our example, the parameters for the initial state of the data, load, and error state do not change, but they are aggregated into a state object managed by a 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 the Dispatch function to send information to the Reducer function. Objects sent using the Dispatch function have mandatory type attributes and optional load attributes. Type tells the Reducer function which state transitions need to be applied, and the Reducer can also use the payload to extract the new state. After all, there are only three state transitions: the initialization fetch process, the result of a data fetch that notifies success, and the result of a data fetch that notifies error.

At the end of the custom hook, the state is returned as before, but because we have a state object instead of a separate state. This way, the person calling the useDataApi custom hook can still access 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

Last but not least, the reducer function implementation is missing. It needs to handle three different state transitions, namely FETCH\ U INIT, FETCH\ U SUCCESS, and FETCH\ U FAILURE. Each state transition needs to return a new state object. Let’s see how to do this with 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, the in-out switch Case statement only returns the previous state for each state transition. The destructuring statement is used to enforce best practices by keeping the state object immutable (meaning that the state never changes directly). Now, let’s override several return properties of the current state to change the state with 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

Each state transition determined by the type of operation now returns a new state based on the previous state and the optional load. For example, the payload is used to set the data for the new state object in the case of a successful request.

In summary, the Reducer hook ensures that this part of state management is encapsulated in its own logic. By providing operation types and optional payloads, you always end up with a predicate state change. In addition, you will never encounter an invalid state. For example, you might have accidentally set the isLoading and isError states to true in the past. What should be displayed in the UI in this case? Each state transition defined by the Reducer function now results in a valid state object.

Abort data extraction takes effect

A common problem with React is setting the component state even if it has been uninstalled (for example, leaving due to using the React router). I’ve written about this problem here before, and it describes how to prevent setting state for uninstalled components in various scenarios. Let’s look at how to prevent setting state in custom hooks for data fetching:

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

Each Effect hook has a clean-up function that runs when the component is unloaded. The clean-up function is a function that is returned from the hook. In our example, we use a Boolean flag called didCancel to let our data retrieval logic know the status (mount/unload) of the component. If the component does unload, the flag should be set to true, which will cause the component state to be unable to be set after the asynchronous parse data is retrieved.

Note: In fact, data retrieval is not aborted — this can be done by Axios cancellation — but state transitions are no longer performed for components that are not installed. Since Axios cancelling is not the best API in my opinion, the Boolean flag that prevents setting state also works.

End: Now you’ve learned how to get data in React hooks. If you’re curious about getting data in class components (and function components) using render items and higher-order components, check out my other article right from the start. Otherwise, I hope this article will be useful for you to learn React hooks and how to use them in real-world scenarios.

Click on the source article to see the original article.

Translation: Zhao Hui