Back to blog overview

January 5, 2021

Cleaner React: Refactoring Hooks

Matti Salokangas

&

&

Senior Software Engineer
Grants Pass, OR

One of the more common things you will do when writing React components is some sort of http request to get data or to update some data. This will usually involve some combination of the useEffect and useState hooks provided to us by the core React library. Now, to be fair there are many ways to do this and useEffect + useState is one of many. The refactoring tips to follow are taking a look at these specifically, but they are applicable to any scenario where there are relationships between data and there is some level of complexity.

To begin, let's consider a few examples and take a look at some pitfalls before moving on to refactoring.

### useEffect + useState

This is about the simplest way you can call an api and keep track of the data in state.

```language-javascript
const [data, setData] = useState([]);
 useEffect(() => {
   getData().then((response) => setData(response));
 }, []);
```

This a good place to start, however one thing to consider is what happens when "setData(response.data)" is called after the user navigates away and the component is unmounted?

Have you ever seen see an error that says something like "Warning: Can't call setState on an unmounted component."? We can fix this by keeping track of whether or not the component is mounted.

### useEffect + useState with unmount protection

The example below uses the "useRef" hook to keep track of when the component is mounted and when it is unmounted. We will prevent any warnings about calling setState on an unmounted component because we are checking to see if our "isMounted" ref is still "true" before we call "setData".

```language-javascript
const isMounted = useRef(true);
 const [data, setData] = useState([]);
 useEffect(() => {
   getData().then((response) => isMounted.current && setData(response));
   return () => { isMounted.current = false; }
 }, []);
```

Sweet! So, we now have a fairly safe way to call our api and keep track of the response. What if we needed to call another api though? Or what if we needed to get data from one api to make another api call? It's these scenarios where our seemingly benign useEffect and useState hooks start to break down. This is the time when one should consider refactoring out to a custom hook.

To begin, let's start off with a complex example that uses a couple state values and a useEffect to set them.

```language-javascript

const isMounted = useRef(true);
 const [weatherPattern, setWeatherPattern] = useState([]);
 const [airPollution, setAirPollution] = useState([]);

 useEffect(() => {
   getWeather().then(
     (response) => isMounted.current && setWeatherPattern(response)
   );
   getAirPollution().then(
     (response) => isMounted.current && setAirPollution(response)
   );
   return () => (isMounted.current = false);
 }, []);
```

Looking at the example above, we can start to see an emerging abstraction with weather pattern and air pollution. Both of these are related and let's consider that we want to show some data when we have both of these values. This raises some interesting questions, like...

  • How do we manage a loading flag?
  • What if one of those calls throws an error?

It's these common themes that I tend to run into that are triggers to me that I may need to extract this out to a custom hook. In addition to that we have related things going on, so it seems to make sense that we'd refactor this out to some sort of weather hook, like "useWeather()" perhaps?

### useWeather Hook

Here's an initial pass at encapsulating the weather-related complexity to a hook.

```language-javascript

const useWeather = () => {
 const isMounted = useRef(true);
 const [weatherPattern, setWeatherPattern] = useState([]);
 const [airPollution, setAirPollution] = useState([]);

 useEffect(() => {
   getWeather().then(
     (response) => isMounted.current && setWeatherPattern(response)
   );
   getAirPollution().then(
     (response) => isMounted.current && setAirPollution(response)
   );
   return () => (isMounted.current = false);
 }, []);

 return {
   airPollution,
   weatherPattern,
 };
};
```

Looking at the usage for this hook, we can begin to see immediate value. Less code. Complexity encapsulated. ✅

```language-javascript

const { airPollution, weatherPattern } = useWeather();
```

#### Introducing React-Query

Let's revisit our hook and see how we can clean things up. As mentioned earlier, we are not providing any sort of loading flag or error indicator. This is where other libraries can come in and make a large impact without needing to write a bunch of code. One library that I have been enjoying lately is react-query. So, let's see what we can do with some help from our awesome open source friends.

I am making use of the "useQuery" hook to call my api, no need for "useEffect" or "useState" anymore. I also get error and loading states for free since the "useQuery" hook returns this for me.

```language-javascript

const useWeather = () => {
 const airPollutionResults = useQuery("airPollution", getAirPollution);
 const weatherResults = useQuery("weather", getWeather);

 const isLoading = weatherResults.isLoading || airPollutionResults.isLoading;
 const isError = weatherResults.isError || airPollutionResults.isError;
 const errors = [airPollutionResults.error, weatherResults.error].filter(
   Boolean
 );

 let airPollution = null;
 let weatherPattern = null;

 if (!isLoading && !isError) {
   airPollution = airPollutionResults.data;
   weatherPattern = weatherResults.data;
 }

 return {
   airPollution,
   errors,
   isError,
   isLoading,
   weatherPattern,
 };
};
```

#### Additional Refactoring

What we have works. But, we can make things a bit more declarative by extracting out the loading and error logic to functions. Going forward, if I want to query more data I don't want to keep adding more or conditions, that will get messy.

```language-javascript

const mapKey = (key, results) =>
 results.map((result) => result[key]).filter(Boolean);

const any = (key, results) => mapKey(key, results).length > 0;

export const useWeather = () => {
 const results = [
   useQuery("airPollution", getAirPollution),
   useQuery("weather", getWeather),
 ];

 const errors = mapKey("error", results);
 const isError = any("isError", results);
 const isLoading = any("isLoading", results);

 const [airPollution, weather] = results;

 return {
   airPollution: airPollution.data,
   errors,
   isError,
   isLoading,
   weatherPattern: weather.data,
 };
};

```

I think we could keep refactoring this, but considering where we started I think we are on the right track.

### Refactoring Hooks

As you are adding more useEffect and useState hooks, keep in mind the complexity and relationships. Try to find those emerging abstractions and do not be afraid to extract a custom hook. It does take more time, but in the long run you will not regret it! Happy refactoring!

The examples from this blog post are [available to view or play around with here](https://github.com/sturdynut/refactoring-hooks).

Let's Chat

Are you ready to build something brilliant? We're ready to help.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
RedwoodJS Logo
RedwoodJS
Conference

conference
for builders

Grants Pass, Oregon • September 26 - 29, 2023
View All