How to retrieve data using React Hooks

  • How to fetch data with React Hooks?
  • Original article by Robin Wieruch
  • Use Data API hook
  • How do I get data from ‘React Hooks’
  • Fetch data with React hooks
  • StackblitzAddress:fetch data with react hooks

preface

  • Note: the author will useTypeScriptTo complete the code in this article, and useant designTo beautify the interface style
  • If the article is useful to you, welcome ‘star’

In this tutorial, I want to show you how to get data through state and Effect hooks. We’ll use the well-known Hacker News API to get popular articles from the tech world. Through this article, you can also implement your own custom hook for data retrieval, which can be reused anywhere in your application or published to NPM as a standalone Node package.

If you don’t know anything about this new React feature, check it out here: Introduction to React Hooks. If you want to see a complete project on how to use hooks in React to get data samples, check out this GitHub repository.

If you just want to prepare to use React Hook to get data: execute NPM install use-data-api and follow the documentation to use it. If you use it, don’t forget to click a star for it.

Note: In the future React Hooks are not intended to be used to retrieve data. Instead, a feature called Suspense takes care of it. However, the following tutorial is a great way to learn more about state and Effect hooks in React.

Use ‘React Hooks’ to get data

If you’re not familiar with how to get data in React, check out my article how to get data in React. It will show you how to use React class Components for data fetching, how to use Render Prop Components and higher-order Components to make code reusable, how to handle errors and load loading states. In this article, I want to show you that using the React Hooks in function components.

import React, { useEffect, useState } from 'react';



const App: React.FC = (a)= > {

  const [data, setData] = useState<{ hits: any[] }>({ 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 all the projects (hits = Hacker News Articles). The state and update state functions come from a hook called useState, which manages the local state of the data we fetch for the App component. The initial state represents the list data with a hollow HITS array of objects, for which no state has been set.

We’ll use Axios to fetch the data, but it’s up to you to use another fetch library or the browser’s native FETCH API. If you haven’t already installed AXIos, you can do this from the command line with NPM install Axios, and then implement effect Hook to get the data.

import React, { useEffect, useState } from 'react';

import axios from 'axios';



const App: React.FC = (a)= > {

  const [data, setData] = useState<{ hits: any[] }>({ hits: []});

  useEffect(async() = > {

    const result = await axios('https://hn.algolia.com/api/v1/search?query=react');

    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

An Effect hook, called useEffect, can use AXIos to get data from the API and use the update function of the component’s State hook to set the data for the local state of that component. Here we use async/await to resolve asynchronous promises.

However, when you run your application, you should get stuck in a nasty loop. Effect Hook runs not only after a component is mounted, but also after a component is updated. Since we set state with setData after we get the data, this updates the component and causes Effect to run again. When Effect runs again, it retrieves the data again and calls setData, all over again. This is a bug to avoid, we only want to get the data after the component is rendered. This is why effect Hook is passed an empty array ([]) as the second parameter: To avoid effect Hook being activated after the component is updated and only after the component is mounted.

import React, { useEffect, useState } from 'react';

import axios from 'axios';



const App: React.FC = (a)= > {

  const [data, setData] = useState<{ hits: any[] }>({ hits: []});

  useEffect(async() = > {

    const result = await axios('https://hn.algolia.com/api/v1/search?query=react');

    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 defines all the variables (assigned to the array) that the hook depends on. If one of the variables changes, the hook will run again. If the variables in the array are empty, the hook will not run after the component is updated, because it does not need to monitor any variables.

There is one last trap. In the code, we use async/await to get data from third-party apis. According to the documentation, each async labeled function returns an implicit promise: “Async function declaration defines an asynchronous function that returns an AsyncFunction object. An asynchronous function is a function that executes asynchronously through an event loop and returns the result using an implicit promise. However, an Effect Hook should either return nothing or a cleanup function. That’s why you might see the following Warning in your developer console log: 07:41:22.910 index.js 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(useEffect must return a cleanup function or nothing at all. Promises and useEffect(async() =>…) Promises and useEffect(async() =>…) ) is not supported, but you can call an async function inside effect. This is why using the async keyword directly in useEffect functions is not allowed. Let’s think of a workaround for this situation: use async functions inside effect. (The code below uses Ant Design, will not be prompted later)

import React, { useEffect, useState } from 'react';

import axios from 'axios';

import { Card, List } from 'antd';

import { IData } from '@/responseTypes';



const App: React.FC = (a)= > {

  const [data, setData] = useState<IData>({ hits: []});

  useEffect((a)= > {

    const fetchData = async() = > {

      const result = await axios('https://hn.algolia.com/api/v1/search?query=react');

      setData({ hits: result.data.hits });

    };

    fetchData().then();

} []);

  return (

    <Card bordered={false}>

      <List dataSource={data.hits} renderItem={(item) => (

        <List.Item>

          <a href={item.url}>{item.title}</a>

        </List.Item>

)} / >

    </Card>

  );

};

export default App;

Copy the code

Hacker News’s response data TypeScript is as follows:

// responseTypes.ts

export interface IData {hits: IHit[]; }



interface IHit {

  title: string;

  url: string;

  author: string;

  points: number;

story_text? : any;

comment_text? : any;

  _tags: string[];

  num_comments: number;

  objectID: string;

  _highlightResult: IHighlightResult;

}



interface IHighlightResult {

  title: ITitle;

  url: ITitle;

  author: IAuthor;

}



interface IAuthor {

  value: string;

  matchLevel: string;

  matchedWords: string[];

}



interface ITitle {

  value: string;

  matchLevel: string;

  matchedWords: any[];

}

Copy the code

In simple terms, this is using React hooks to get data. But if you’re interested in error handling, showing loading state, how to trigger fetching data from a form, and how to implement a reusable data fetching hook, read on.

How do I trigger a ‘Hook’ manually/programmatically

Good, we’ll get the data once the component is mounted. But what about using an input field to tell the API which topic we’re interested in?” Redux” is used as the default query, but what about “React”? Next, let’s implement an input box that lets the user get related articles other than “Redux”. Therefore, we introduce a new state for the input element.

import React, { useEffect, useState } from 'react';

import axios from 'axios';

import { Card, Input, List } from 'antd';

import { IData } from '@/responseTypes';



const App: React.FC = (a)= > {

  const [data, setData] = useState<IData>({ hits: []});

  const [query, setQuery] = useState('redux');

  useEffect((a)= > {

    const fetchData = async() = > {

      const result = await axios('https://hn.algolia.com/api/v1/search?query=redux');

      setData({ hits: result.data.hits });

    };

    fetchData().then();

} []);

  return (

    <Card bordered={false}>

      <Input

        type="text"

        value={query}

        onChange={(e) => setQuery(e.target.value)}

      />

      <List dataSource={data.hits} renderItem={(item) => (

        <List.Item>

          <a href={item.url}>{item.title}</a>

        </List.Item>

)} / >

    </Card>

  );

};

export default App;

Copy the code

At this point, the two states are independent of each other, but now you want to combine them to fetch only the article data specified in the input field. Once the component is mounted, all articles should be retrieved through the query item.

import React, { useEffect, useState } from 'react';

import axios from 'axios';

import { Card, Input, List } from 'antd';

import { IData } from '@/responseTypes';



const App: React.FC = (a)= > {

  const [data, setData] = useState<IData>({ hits: []});

  const [query, setQuery] = useState('redux');

  useEffect((a)= > {

    const fetchData = async() = > {

      const result = await axios(`https://hn.algolia.com/api/v1/search?query=${query}`);

      setData({ hits: result.data.hits });

    };

    fetchData().then();

} []);

  return (

    <Card bordered={false}>

      <Input

        type="text"

        value={query}

        onChange={(e) => setQuery(e.target.value)}

      />

      <List dataSource={data.hits} renderItem={(item) => (

        <List.Item>

          <a href={item.url}>{item.title}</a>

        </List.Item>

)} / >

    </Card>

  );

};

export default App;

Copy the code

Here’s one thing that’s missing: when you try to type some text in the input box, the component doesn’t fetch data when it’s re-rendered. That’s because you provided an empty array as the second argument to Effect, which doesn’t depend on any variables, so it only fires after the component is mounted. However, Effect should now rely on Query, and if Query changes, the data request will fire again.

import React, { useEffect, useState } from 'react';

import axios from 'axios';

import { Card, Input, List } from 'antd';

import { IData } from '@/responseTypes';



const App: React.FC = (a)= > {

  const [data, setData] = useState<IData>({ hits: []});

  const [query, setQuery] = useState('redux');

  useEffect((a)= > {

    const fetchData = async() = > {

      const result = await axios(`https://hn.algolia.com/api/v1/search?query=${query}`);

      setData({ hits: result.data.hits });

    };

    fetchData().then();

  }, [query]);

  return (

    <Card bordered={false}>

      <Input

        type="text"

        value={query}

        onChange={(e) => setQuery(e.target.value)}

      />

      <List dataSource={data.hits} renderItem={(item) => (

        <List.Item>

          <a href={item.url}>{item.title}</a>

        </List.Item>

)} / >

    </Card>

  );

};

export default App;

Copy the code

Once you change the value in the input box the data will be retrieved. But this causes another problem: every time you type a character into the input field, effect will be triggered and another data fetch request will be executed. So how do you provide a button to trigger the request to manually execute the hook?

import React, { useEffect, useState } from 'react';

import axios from 'axios';

import { Button, Card, Input, List } from 'antd';

import { IData } from '@/responseTypes';



const App: React.FC = (a)= > {

  const [data, setData] = useState<IData>({ hits: []});

  const [query, setQuery] = useState('redux');

  const [search, setSearch] = useState('redux');

  useEffect((a)= > {

    const fetchData = async() = > {

      const result = await axios(`https://hn.algolia.com/api/v1/search?query=${search}`);

      setData({ hits: result.data.hits });

    };

    fetchData().then();

  }, [search]);

  return (

    <Card bordered={false}>

      <Input

        type="text"

        value={query}

        onChange={(e) => setQuery(e.target.value)}

      />

      <Button onClick={() => setSearch(query)}>Search</Button>

      <List dataSource={data.hits} renderItem={(item) => (

        <List.Item>

          <a href={item.url}>{item.title}</a>

        </List.Item>

)} / >

    </Card>

  );

};

export default App;

Copy the code

We now make effect depend on the Search state rather than the Query state, which changes with each keystroke in the input box. Once the user clicks the button, the new search state is set and effect Hook is triggered manually.

The initial value of the search state is also set to the same as the Query state. Because the component also retrieves data after it is mounted, the result should be the same as the value in the input box as the query criteria. However, having similar Query and search states is a bit confusing. Why not set the current request address as the state instead of the search state?

import React, { useEffect, useState } from 'react';

import axios from 'axios';

import { Button, Card, Input, List } from 'antd';

import { IData } from '@/responseTypes';



const App: React.FC = (a)= > {

  const [data, setData] = useState<IData>({ hits: []});

  const [query, setQuery] = useState('redux');

  const [url, setUrl] = useState('https://hn.algolia.com/api/v1/search?query=redux');

  useEffect((a)= > {

    const fetchData = async() = > {

      const result = await axios(url);

      setData({ hits: result.data.hits });

    };

    fetchData().then();

  }, [url]);

  return (

    <Card bordered={false}>

      <Input

        type="text"

        value={query}

        onChange={(e) => setQuery(e.target.value)}

      />

      <Button onClick={() => setUrl(`https://hn.algolia.com/api/v1/search?query=${query}`)}>Search</Button>

      <List dataSource={data.hits} renderItem={(item) => (

        <List.Item>

          <a href={item.url}>{item.title}</a>

        </List.Item>

)} / >

    </Card>

  );

};

export default App;

Copy the code

The above example is using Effect Hook to retrieve data programmatically/manually. You can decide which state effect depends on. Once you setState in the click event or other useEffect, the corresponding effect will run again. In the example above, if the URL state changes, Effect will run again and fetch the article data from the API.

Use ‘React Hooks’ to show’ loading ‘

Let’s continue with the loading demonstration of data retrieval. In essence, loading is just another state managed by the state hook. In the App component, the loading flag is used to render a loading indicator.

import React, { useEffect, useState } from 'react';

import axios from 'axios';

import { Button, Card, Input, List } from 'antd';

import { IData } from '@/responseTypes';



const App: React.FC = (a)= > {

  const [data, setData] = useState<IData>({ 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((a)= > {

    const fetchData = async() = > {

      setIsLoading(true);

      const result = await axios(url);

      setData({ hits: result.data.hits });

      setIsLoading(false);

    };

    fetchData().then();

  }, [url]);

  return (

    <Card bordered={false}>

      <Input

        type="text"

        value={query}

        onChange={(e) => setQuery(e.target.value)}

      />

      <Button onClick={() => setUrl(`https://hn.algolia.com/api/v1/search?query=${query}`)}>Search</Button>

      <List dataSource={data.hits} loading={isLoading} renderItem={(item) => (

        <List.Item>

          <a href={item.url}>{item.title}</a>

        </List.Item>

)} / >

    </Card>

  );

};

export default App;

Copy the code

When a component is mounted or the URL state changes, Effect is called to get data, and the loading state is set to true. Once the request successfully retrieves data, the loading state is set to false again.

Use ‘React Hooks’ to handle errors

How to use React Hook to handle errors for data retrieval? Error is just another state initialized with the State hook. Once there is an error state, the App component will render the error page for the user. It is common to use try/catch for error handling when using async/await, you can do this in effect.

import React, { useEffect, useState } from 'react';

import axios from 'axios';

import { Button, Card, Input, List } from 'antd';

import { IData } from '@/responseTypes';



const FetchData: React.FC = (a)= > {

  const [data, setData] = useState<IData>({ 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((a)= > {

    const fetchData = async() = > {

      setIsError(false);

      setIsLoading(true);

      try {

        const result = await axios(url);

        setData({ hits: result.data.hits });

      } catch (e) {

        setIsError(true);

      }

      setIsLoading(false);

    };

    fetchData().then();

  }, [url]);

  return (

    <Card bordered={false}>

      <Input

        type="text"

        value={query}

        onChange={(e) => setQuery(e.target.value)}

      />

      <Button onClick={() => setUrl(`https://hn.algolia.com/api/v1/search?query=${query}`)}>Search</Button>

      {isError ?

<div>something went wrong... </div>

        :

        <List dataSource={data.hits} loading={isLoading} renderItem={(item) => (

          <List.Item>

            <a href={item.url}>{item.title}</a>

          </List.Item>

)} / >}

    </Card>

  );

};

export default FetchData;

Copy the code

The error status will be reset each time the hooks run again. This is useful because the user might want to try again after a failed request, at which point the error should be reset. To force an error you can change the URL to some invalid value and then check to see if the error message is displayed.

Get data from the form in ‘React’

How do you get data from a proper form? So far, we’ve only had input fields and buttons combined. Once you want to introduce more input elements, you might want to wrap them with a form element. In addition, form lets you trigger the Search button with the Enter key.

import React, { useEffect, useState } from 'react';

import axios from 'axios';

import { Button, Card, Input, List } from 'antd';

import { IData } from '@/responseTypes';



const App: React.FC = (a)= > {

  const [data, setData] = useState<IData>({ 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((a)= > {

    const fetchData = async() = > {

      setIsError(false);

      setIsLoading(true);

      try {

        const result = await axios(url);

        setData({ hits: result.data.hits });

      } catch (e) {

        setIsError(true);

      }

      setIsLoading(false);

    };

    fetchData().then();

  }, [url]);

  return (

    <Card bordered={false}>

      <form onSubmit={(e) => {

        setUrl(`https://hn.algolia.com/api/v1/search?query=${query}`);

}} >

        <Input

          type="text"

          value={query}

          onChange={(e) => setQuery(e.target.value)}

        />

        <Button htmlType="submit">Search</Button>

      </form>

      {isError ?

<div>something went wrong... </div>

        :

        <List dataSource={data.hits} loading={isLoading} renderItem={(item) => (

          <List.Item>

            <a href={item.url}>{item.title}</a>

          </List.Item>

)} / >}

    </Card>

  );

};

export default App;

Copy the code

But now when the submit button is clicked, the browser will reload, which is the default behavior of the browser when submitting a form. To prevent the default behavior, we call a function inside the React event. You can do the same with the React class component.

import React, { useEffect, useState } from 'react';

import axios from 'axios';

import { Button, Card, Input, List } from 'antd';

import { IData } from '@/responseTypes';



const FetchData: React.FC = (a)= > {

  const [data, setData] = useState<IData>({ 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((a)= > {

    const fetchData = async() = > {

      setIsError(false);

      setIsLoading(true);

      try {

        const result = await axios(url);

        setData({ hits: result.data.hits });

      } catch (e) {

        setIsError(true);

      }

      setIsLoading(false);

    };

    fetchData().then();

  }, [url]);

  return (

    <Card bordered={false}>

      <form onSubmit={(e) => {

        e.preventDefault();

        setUrl(`https://hn.algolia.com/api/v1/search?query=${query}`);

}} >

        <Input

          type="text"

          value={query}

          onChange={(e) => setQuery(e.target.value)}

        />

        <Button htmlType="submit">Search</Button>

      </form>

      {isError ?

<div>something went wrong... </div>

        :

        <List dataSource={data.hits} loading={isLoading} renderItem={(item) => (

          <List.Item>

            <a href={item.url}>{item.title}</a>

          </List.Item>

)} / >}

    </Card>

  );

};

export default FetchData;

Copy the code

The browser will no longer reload when you click submit. It works just as it did before, but now it’s a form instead of a combination of native input fields and buttons, and you can also press enter on your keyboard to submit form content.

Custom data acquisition ‘Hook’

To extract a custom hook for fetching data, move all data fetching code, including loading display and error handling, into its own functions, in addition to the query state belonging to the input box. Of course, be sure to return all necessary variables used in App components from the function.

The initial state can also be made generic by simply passing it to a new custom hook.

import { Dispatch, SetStateAction, useEffect, useState } from 'react';

import axios from 'axios';



interface IResult<T> {

  data: T;

  isLoading: boolean;

  isError: boolean;

}



const useHackerNewsApi = <T extends any> (initialData: T, initialUrl: string): [IResult<T>, Dispatch<SetStateAction<string>>] => {

  const [data, setData] = useState<T>(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 (e) {

        setIsError(true);

      }

      setIsLoading(false);

    };

    fetchData().then();

  }, [url]);

  return [{ data, isLoading, isError }, setUrl];

};



export default useHackerNewsApi;

Copy the code

Tip: Arrow functions define generic syntax in TSX

Now your new hook can be used again in the App component:

import React, { useState } from 'react';

import { Button, Card, Input, List } from 'antd';

import useHackerNewsApi from '@/views/fetchData/useHackerNewsApi';

import { IData } from '@/responseTypes';



const FetchData: React.FC = (a)= > {

  const [query, setQuery] = useState('redux');

  const [{ data, isLoading, isError }, setUrl] = useHackerNewsApi<IData>({ hits: []},'https://hn.algolia.com/api/v1/search?query=redux');

  return (

    <Card bordered={false}>

      <form onSubmit={(e) => {

        e.preventDefault();

        setUrl(`https://hn.algolia.com/api/v1/search?query=${query}`);

}} >

        <Input

          type="text"

          value={query}

          onChange={(e) => setQuery(e.target.value)}

        />

        <Button htmlType="submit">Search</Button>

      </form>

      {isError ?

<div>something went wrong... </div>

        :

        <List dataSource={data.hits} loading={isLoading} renderItem={(item) => (

          <List.Item>

            <a href={item.url}>{item.title}</a>

          </List.Item>

)} / >}

    </Card>

  );

};

export default FetchData;

Copy the code

This is to use a custom hook to get the data. The hook itself doesn’t know anything about the API, it receives all the parameters from outside, and only manages the necessary states, such as: data,loading,error. It acts like a custom request data hook to initiate requests and return data to components that use it.

Obtain data using ‘Reducer hook’

So far, we have used multiple state hooks to manage our data acquisition state data, loading state, and error state. However, all of these states managed with separate State hooks should belong to the same class, because they care about the same problem. As you can see, they are both used in data retrieval functions. They are used one after another (e.g., setIsError,setIsLoading), which is a good indication that they are together. Let’s combine all three states with Reducer Hook.

The Reducer Hook returns us a state object and a function that changes the state object. This function, called the dispatch function, takes an action with type and an optional payload as parameters. All this information is used to extract a new state from the previous state and the optional payload and type of the action. 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, {

    isLoadingfalse.

    isErrorfalse.

    data: initialData,

  });

.

};

Copy the code

The Reducer hook takes the Reducer function and an initial state object as parameters. In our example, the initial parameters of data,loading, and error states do not change, but they are aggregated into a state object, and each individual state hook is replaced by a Reducer hook.

const dataFetchReducer = (state, action) = > {

.

};

const useDataApi = (initialUrl, initialData) = > {

  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {

    isLoadingfalse.

    isErrorfalse.

    data: initialData,

  });

  useEffect((a)= > {

    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 data is fetched, the Dispatch function sends information to the Reducer function. An object sent using the dispatch function has a required type attribute and an optional payload attribute. Type tells the reducer function which state transition needs to be applied, and payload is used to extract the new state from the reducer. In the end, we only have three state transitions: initialization of the data retrieval process, notification of success of the data retrieval result, and notification of exception of the data retrieval result.

At the end of the custom hook, state is returned as before, because we have one state object instead of several independent states. In this way, components that call useDataApi custom hooks can still use data, isLoading, and isError.

const useDataApi = (initialUrl, initialData) = > {

  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {

    isLoadingfalse.

    isErrorfalse.

    data: initialData,

  });

.

  return [state, setUrl];

};

Copy the code

Last but not least, we are missing the reducer function implementation. It needs to handle three different state transitions: FETCH_INIT,FETCH_SUCCESS, and FETCH_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(a);

  }

};

Copy the code

The Reducer function has parameters to access the current state and the actions passed in during dispatch. So far, each state transition in the Switch Case statement only returns the previous state. The expansion syntax is used to enforce best practices by ensuring that state objects are immutable (meaning that state can never be changed directly). Now let’s override some return properties of the current state to change the state of each state change.

const dataFetchReducer = (state, action) = > {

  switch (action.type) {

    case 'FETCH_INIT':

      return {

. state,

        isLoadingtrue.

        isErrorfalse

      };

    case 'FETCH_SUCCESS':

      return {

. state,

        isLoadingfalse.

        isErrorfalse.

        data: action.payload,

      };

    case 'FETCH_FAILURE':

      return {

. state,

        isLoadingfalse.

        isErrortrue.

      };

    default:

      throw new Error(a);

  }

};

Copy the code

Now, each state transition determined by the action’s type will return a new state based on the previous state and the optional payload. For example, in the case of a successful request, payload is used to set the data property of the new state object.

In summary, the Reducer Hook ensures that this part of state management is encapsulated with its own logic. By providing action types and optional payload, you will always update the state with a predictable state change. In addition, you will never encounter an invalid state. For example, before this, isLoading and isError states could be accidentally set to true. What should the UI display in this case? Each state change defined by the Reducer function now points to a valid state object.

Note: The source code for all custom hooks is as follows

import { Dispatch, Reducer, SetStateAction, useEffect, useReducer, useState } from 'react';

import axios from 'axios';



interface IResult<T = any> {

  data: T;

  isLoading: boolean;

  isError: boolean;

}



type IAction<T = any> = {

  type'FETCH_INIT';

} | {

  type'FETCH_SUCCESS';

  payload: T

} | {

  type'FETCH_FAILURE';

}

const dataFetchReducer = <T extends any> (state: IResult<T>, action: IAction<T>): IResult<T> => {

  switch (action.type) {

    case 'FETCH_INIT':

return { ... state, isLoading: true };

    case 'FETCH_SUCCESS':

      return {

. state,

        data: action.payload,

        isLoading: false,

        isError: false

      };

    case 'FETCH_FAILURE':

return { ... state, isError: true };

    default:

      throw new Error();

  }

};



const useDataApi = <T extends any> (initialData: T, initialUrl: string): [IResult<T>, Dispatch<SetStateAction<string>>] => {

  const [state, dispatch] = useReducer<Reducer<IResult<T>, IAction<T>>>(dataFetchReducer, {

    data: initialData,

    isError: false,

    isLoading: false

  });

  const [url, setUrl] = useState(initialUrl);

  useEffect(() => {

    const fetchData = async () => {

      dispatch({ type: 'FETCH_INIT' });

      try {

        const result = await axios(url);

        dispatch({ type: 'FETCH_SUCCESS', payload: result.data });

      } catch (e) {

        dispatch({ type: 'FETCH_FAILURE' });

      }

    };

    fetchData().then();

  }, [url]);

  return [state, setUrl];

};



export { useDataApi };

Copy the code

Use this in a component:

import React, { useState } from 'react';

import { Button, Card, Input, List } from 'antd';

import { useDataApi } from '@/views/fetchData/useHackerNewsApi';

import { IData } from '@/responseTypes';



const FetchData: React.FC = (a)= > {

  const [query, setQuery] = useState('redux');

  const [{ data, isLoading, isError }, setUrl] = useDataApi<IData>({ hits: []},'https://hn.algolia.com/api/v1/search?query=redux');

  return (

    <Card bordered={false}>

      <form onSubmit={(e) => {

        e.preventDefault();

        setUrl(`https://hn.algolia.com/api/v1/search?query=${query}`);

}} >

        <Input

          type="text"

          value={query}

          onChange={(e) => setQuery(e.target.value)}

        />

        <Button htmlType="submit">Search</Button>

      </form>

      {isError ?

<div>something went wrong... </div>

        :

        <List dataSource={data.hits} loading={isLoading} renderItem={(item) => (

          <List.Item>

            <a href={item.url}>{item.title}</a>

          </List.Item>

)} / >}

    </Card>

  );

};

export default FetchData;

Copy the code

Terminate data retrieval in ‘effect hook’

This is a common problem in React where the component state is set even after it has been uninstalled (for example, by navigating away from the current component using the React Router). I’ve written about this issue here before, and it describes how to prevent unloading component setup state in various scenarios. Let’s look at how to prevent setting state in a custom hook that gets data:

const useDataApi = <T extends any> (initialData: T, initialUrl: string): [IResult<T>, Dispatch<SetStateAction<string>>] => {

  const [state, dispatch] = useReducer<Reducer<IResult<T>, IAction<T>>>(dataFetchReducer, {

    data: initialData,

    isError: false,

    isLoading: false

  });

  const [url, setUrl] = useState(initialUrl);

  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 (e) {

if (! didCancel) {

          dispatch({ type: 'FETCH_FAILURE' });

        }

      }

    };

    fetchData().then();

    return () => {

      didCancel = true;

    };

  }, [url]);

  return [state, setUrl];

};

Copy the code

The cleanup function corresponding to each Effect Hook is run when the component is uninstalled. A cleanup function is a function that is returned from a hook. In our example, we use a Boolean value called didCancel as an identifier to let the data retrieval logic know the status (mount/unload) of the component. If the component has been uninstalled, the flag should be set to true to prevent the state of the component from being set after the data has been asynchronously parsed.

Note: The data retrieval is not actually terminated (this can be done using Axios Cancellation), but the mounted component no longer performs a state transition. Since the Axios Cancellation is not in my opinion the best API, this Boolean value can also do the job of preventing the component from setting up state.