Refactoring An Old React App: Creating a Custom Hook to Make Fetch-Related Logic Reusable
Tags: react, hooks, refactoring
Published: 2019-09-30 01:14:50

I recently picked up an old project from two years ago. The app is not a very complicated one - it reads data from a simple API server and present them to the users, pretty standard stuff. The client has been pretty happy about the results so now they have come back with more feature requirements they'd like to include in the next iteration.

## The Old Fashioned Way

Before actually start working on those features, I decided to bring all the dependencies up-to-date (it was still running `React 16.2` - feels like eons ago) and do some "house cleaning". I'm glad that me from 2 years ago took the time to write plenty of unit and integration tests so this process was mostly painless. However when I was migrating those old React lifecycle functions (`componentWill*` series) to newer ones, a familiar pattern emerged:

```jsx
class FooComponent extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      isLoading: true,
      error: null,
      data: null,
    };
  }

  componentDidMount() {
    this.fetchData();
  }

  componentDidUpdate(prevProps) {
    if (prevProps.fooId !== this.props.fooId) {
      this.fetchData();
    }
  }

  fetchData() {
    const url = compileFooUrl({ fooId: this.props.fooId });

    fetch(url).then(
      // set data on state
    ).catch(
      // set error on state
    );
  }

  render() {
    // redacted. 
  }
}
```

Does this look familiar to you? The `FooComponent` here fetches `foo` from a remote source and renders it. A new `foo` will be fetched when the `fooId` in the props changes. We are also using some state field to track the request and the data fetched.

In this app I'm trying to improve, this pattern is seen in multiple components, but before `hooks`, it's often not very straight forward to share logic like this, but not anymore! Let's try to create a re-usable hook to improve our code.

## First Iteration With Hooks

Now before we actually write a reusable custom hook, let's try to refactor this component. I think it's pretty obvious that we are going to need `useState` to replace `this.state` and let `useEffect` handle the data fetching part. `useState` is pretty easy to handle, but if you are not familiar with `useEffect` yet, Dan Abramov has a really good (and lengthy) blog article about it: https://overreacted.io/a-complete-guide-to-useeffect/

Our *hooked* component now looks like this:

```jsx
const FooComponent = ({ fooId }) => {
  const [state, setState] = useState({
    isLoading: true,
    error: null,
    data: null,
  });

  useEffect(() => {
    const url = compileFooUrl({ fooId });

    fetch(url)
      .then((response) => {
        if (response.ok) {
          return response.json().then(data => {
            setState({
              isLoading: false,
              data,
              error: null,
            });
          });
        }

        return Promise.reject(response);
      })
      .catch(/* similar stuff here */);
  }, [fooId]);

  return (
     // redacted
  );
};
```

Pretty easy, eh? Our component now works almost* exactly like before with fewer lines (and cooler hook functions!), and all integration tests are still green! It fetches `foo` when it *mounts* and re-fetches it when `fooId` changes.

* "almost" -> The component is now a function component which cannot take a `ref`. https://reactjs.org/docs/refs-and-the-dom.html#accessing-refs 

## Making Our Logic Re-useable

The next step would be making this fetch-and-set-state logic re-usable. Luckily it is extremely easy to write a custom hook - we just need to cut-and-paste our code to a separate file!

Let's name our reusable hook `useGet`, which takes an `url` - since apparently not all components are gonna use `foo` and not all getRequests depend on a single ID I think it's probably easier to leave that url-building logic to each component that wants to use our custom hook. Here's what we are aiming for:

```js
const FooComponent = ({ fooId }) => {
  const fooUrl = compileFooUrl({ fooId: this.props.fooId });

  const { isLoading, data, error } = useGet({ url });

  return (
    // same jsx as before
  );
};
```

Let's cut-and-paste:

```js
export function useGet = ({ url }) => {
  const [state, setState] = useState({
    isLoading: true,
    error: null,
    data: null,
  });

  useEffect(() => { /* fetch logic here */}, [url]);

  // return the `state` so it can be accessed by the component that uses this hook.

  return state;
};
```

By the way, `then/catch` is so 2017, let's use `async/await` instead to reduce the nested callbacks - everybody hates those. Unfortunately `useEffect` cannot take an async function at this moment, we will have to define an async function inside of it, and call it right away. Our new `useEffect` looks something like this:

```js
useEffect(() => {
  const fetchData = async () => {
    setState({
      isLoading: true,
      data: null,
      error: null,
    });

    try {
      const response = await fetch(url);

      if (!response.ok) {
        // this will be handled by our `catch` block below
        throw new Error(`Request Error: ${response.status}`);
      }

      setState({
        isLoading: false,
        data: await response.json(),
        error: null,
      });
    } catch(e) {
      setState({
        isLoading: false,
        data: null,
        error: e.message,
      });
    }
  };

  fetchData();
}, [url]);
```

Much easier to read, isn't it?

## The Problem with `useState`

In simple use cases like we have above, `useState` is probably fine, however there is a small problem with our code: we have to provide values to all of the fields in the state object every time we want to use `setState`. And sometimes, we don't necessarily want to reset other fields when a new request is fired (e.g. in some cases we might still want the user to be able to see the previous error message or data when a new request fires). You might be tempted to do this:

```js
setState({
  ...state,
  isLoading: true,
})
```

However that means `state` also becomes a dependency of `useEffect` - and if you add it to the array of dependencies, you will be greeted with an infinite fetch loop because every time `state` changes, React will try to call the effect (which in turn, produces a new state).

Luckily we have `useReducer` - it's somewhat similar to `useState` here but it allows you to separate your state-updating logic from your component. If you have used `redux` before, you already know how it works.

If you are new to the concept, you can think a `reducer` is a function that takes a `state` and an `action` then returns a new `state`. and `useReducer` is a hook that let you define an initial state, a "reducer" function that will be used to update the state. `useReducer` returns the most up-to-date state and a function that you will be using to dispatch actions.

```js
const [state, dispatch] = useReducer(reducerFunction, initialState);
```

Now in our use case here, we've already got our `initialState`:

```js
{
  isLoading: false,
  data: null,
  error: null,
}
```

And our state object is updated when the following action happens:

* Request Started (sets `isLoading` to true)
* Request Successful
* Request Failed

Our reducer function should handle those `actions` and update the state accordingly. In some actions, (like "request successful") we will also need to provide some extra data to the reducer so it can set them onto the state object. An `action` can be almost any value (a string, a symbol, or an object), but in most cases we use objects with a `type` field:

```js
// a request successful action:
{
  type: 'Request Successful', // will be read by the reducer
  data, // data from the api
}
```

To dispatch an action, we simply call `dispatch` with the action object:

```js
const [state, dispatch] = useReducer(reducer, initialState);

// fetch ... and dispatch the action below when it is successful
dispatch({
  type: 'Request Successful'
  data: await response.json(),
});
```

And usually, we use "action creators" to generate those `action` objects so we don't need to construct them everywhere. Action creators also makes our code easier to change if we want to add additional payloads to an action, or rename `type`s. 

```js
// example of action creator:

// a simple function that takes some payload, and returns an action object:
const requestSuccessful = ({ data }) => ({
  type: 'Request Successful',
  data,
}); 
```

Often to avoid typing each `type` string again and again - we can define them separately as constants, so both the action creators and the reducers can re-use them. Typos are very common in programming - typos in strings are often harder to spot, but if you make a typo in a variable or a function call, your editors & browsers will alert you right away. 


```js
// a contants.js file

export const REQUEST_STARTED = 'REQUEST_STARTED';
export const REQUEST_SUCCESSFUL = 'REQUEST_SUCCESSFUL';
export const REQUEST_FAILED = 'REQUEST_FAILED';
export const RESET_REQUEST = 'RESET_REQUEST';
```

```js
// action creators:

export const requestSuccessful = ({ data }) => ({
  type: REQUEST_SUCCESSFUL,
  data,
});
```

```js
// dispatching an action in our component:

dispatch(requestSuccessful({ data: await response.json() }));
```

Now, onto our reducer - it updates the state accordingly for each `action`:

```js
// reducer.js

// a reducer receives the current state, and an action
export const reducer = (state, action) => {
  // we check the type of each action and return an updated state object accordingly
  switch (action.type) {
    case REQUEST_STARTED:
      return {
        ...state,
        isLoading: true,
      };
    case REQUEST_SUCCESSFUL:
      return {
        ...state,
        isLoading: false,
        error: null,
        data: action.data,
      };
    case REQUEST_FAILED:
      return {
        ...state,
        isLoading: false,
        error: action.error,
      };

    // usually I ignore the action if its `type` is not matched here, some people prefer throwing errors here - it's really up to you.
    default:
      return state;
  }
};

```

Putting it together, our hook now looks like this:

```js
// import our action creators
import {
  requestStarted,
  requestSuccessful,
  requestFailed,
} from './actions.js';
import { reducer } from './reducer.js';

export const useGet = ({ url }) => {
  const [state, dispatch] = useReducer(reducer, {
    isLoading: true,
    data: null,
    error: null,
  });

  useEffect(() => {
    const fetchData = async () => {
      dispatch(requestStarted());

      try {
        const response = await fetch(url);

        if (!response.ok) {
          throw new Error(
            `${response.status} ${response.statusText}`
          );
        }

        const data = await response.json();

        dispatch(requestSuccessful({ data }));
      } catch (e) {
        dispatch(requestFailed({ error: e.message }));
      }
    };

    fetchData();
  }, [url]);

  return state;
};
```

`dispatch` is guaranteed to be stable and won't be changed between renders, so it doesn't need to be a dependency of `useEffect`. Now our hook is much cleaner and easier to be reasoned with. 

Now we can start refactoring other components that uses data from a remote source with our new hook!

## But There Is More

We are not done yet! However this post is getting a bit too long. Here's the list of things I'd like to cover in a separate article:

* [Clean up our effect](https://dev.to/pallymore/clean-up-async-requests-in-useeffect-hooks-90h)
* Use hooks in class-components.
* Testing our hooks.
* A "re-try" option. Let's give the user an option to retry when a request fails - how do we do that with our new hook?

Stay tuned!