DEV Community

Miloš Šikić
Miloš Šikić

Posted on

Do React Custom Hooks always need to be reusable?

If you've ever wondered about this, or have been in a situation where you transitioned to hooks and now it seems like you have a lot of code above your JSX and you're wondering how that could be improved - stick around, you might find something useful here.

When I first delved into the world of hooks, I was struggling with the abundance of code that ended up being inside of the components that now did both the presentation and the logic, since Dan Abramov said that the Presentation/Container paradigm is basically all but dead. I asked a friend about it, and he told me that I should make custom hooks in order to clean up the code and make it more organized. What followed was a very enjoyable experience in React once more.

Let's get a baseline. I made a very elaborate example (thanks to the awesome people who provided the Dog API!) to showcase why I think custom hooks are very powerful, even if they are not reusable.

It's a basic page where you can select your favorite breed of dog (if it's listed) and you'll get a random picture of a dog of that breed, as well as a button to look for a different random picture. It's simple, but very effective for what I want to convey.

Here's the code for the main component that you actually see working in the demo.

import React, { useMemo, useState } from "react";
import useSWR from "swr";

import { endpoints, DoggoBreedResponse, DoggoBreedPicResponse } from "../api/";
import { Doggo } from "../doggo";
import { NO_SELECTION } from "../constan";

import styles from "../pickerPage.module.css";

export const PickerPage = () => {
  const { data: doggosResponse } = useSWR<DoggoBreedResponse>(
    endpoints.allBreeds
  );

  const doggos = useMemo<Doggo[] | undefined>(() => {
    if (!doggosResponse) {
      return undefined;
    }

    const allBreeds = Object.keys(doggosResponse.message).map((doggoBreed) => ({
      breedId: doggoBreed,
      breedLabel: doggoBreed.charAt(0).toUpperCase() + doggoBreed.slice(1)
    }));

    const defaultOption: Doggo = {
      breedId: NO_SELECTION,
      breedLabel: "Select your favorite pupper!"
    };

    return [defaultOption, ...allBreeds];
  }, [doggosResponse]);

  const [currentDoggoBreedId, setCurrentDoggo] = useState(NO_SELECTION);

  const { data: doggoPictureResponse, revalidate } = useSWR<
    DoggoBreedPicResponse
  >(
    // If this function returns something falsy, useSWR won't make a request.
    () => currentDoggoBreedId && endpoints.pictureForBreed(currentDoggoBreedId)
  );

  return (
    <div className={styles.container}>
      <span className={styles.header}>What's your favorite doggo breed?</span>
      {doggos && (
        <select
          value={currentDoggoBreedId}
          onChange={({ target: { value } }) => setCurrentDoggo(value)}
          className={styles.select}
        >
          {doggos.map(({ breedId, breedLabel }) => (
            <option key={breedId} value={breedId}>
              {breedLabel}
            </option>
          ))}
        </select>
      )}
      {doggoPictureResponse && (
        <>
          <img
            className={styles.image}
            src={doggoPictureResponse?.message}
            alt="Doggo's pic"
          />
          <button onClick={() => revalidate()}>Get a different picture!</button>
        </>
      )}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

What's the first thing you would optimize here? And I mean for readability. Probably the JSX. And you'd be right, it could be a lot nicer. But today we're here to do the same thing for the hooks inside of this component. Let's dive deeper.

Check out this piece of code.

const { data: doggosResponse } = useSWR<DoggoBreedResponse>(
  endpoints.allBreeds
);

const doggos = useMemo<Doggo[] | undefined>(() => {
  if (!doggosResponse) {
    return undefined;
  }

  const allBreeds = Object.keys(doggosResponse.message).map((doggoBreed) => ({
    breedId: doggoBreed,
    breedLabel: doggoBreed.charAt(0).toUpperCase() + doggoBreed.slice(1)
  }));

  const defaultOption: Doggo = {
    breedId: NO_SELECTION,
    breedLabel: "Select your favorite pupper!"
  };

  return [defaultOption, ...allBreeds];
}, [doggosResponse]);
Enter fullscreen mode Exit fullscreen mode

It does all of the mapping logic to adapt the response to something that our UI can work more easily with. But is it really important to anyone that is trying to comprehend what this component is doing? I'd say it's not. Also, this whole block produces a single variable that we will use in our component, doggos. There's nothing else that we need from this code in the current scope.

Imagine if we had something like this instead, then.

const doggos = useDoggoData();
Enter fullscreen mode Exit fullscreen mode

And would you look at that? We actually can! All we need to do is make a custom hook and literally move our code there.

import { useMemo } from "react";
import useSWR from "swr";

import { endpoints, DoggoBreedResponse } from "../api/";
import { NO_SELECTION } from "../constan";
import { Doggo } from "../doggo";

export const useDoggoData = () => {
  const { data: doggosResponse } = useSWR<DoggoBreedResponse>(
    endpoints.allBreeds
  );

  const doggos = useMemo<Doggo[] | undefined>(() => {
    if (!doggosResponse) {
      return undefined;
    }

    const allBreeds = Object.keys(doggosResponse.message).map((doggoBreed) => ({
      breedId: doggoBreed,
      breedLabel: doggoBreed.charAt(0).toUpperCase() + doggoBreed.slice(1)
    }));

    const defaultOption: Doggo = {
      breedId: NO_SELECTION,
      breedLabel: "Select your favorite pupper!"
    };

    return [defaultOption, ...allBreeds];
  }, [doggosResponse]);

  return doggos;
};
Enter fullscreen mode Exit fullscreen mode

The only difference is that we return doggos; at the end of the hook. But that's it! The code is identical, we just literally yanked it outside of component and into its own file. And now we have a separate piece of code that has one duty, and that is to process the fetched data from the server and adapt it for our View to handle.

Now, the developer doesn't need to immediately think about what all of this code does, because it's outside of the component. If they're not interested in the mapping part of the logic, they'll just skim over this hook and say "Ah, ok, we're fetching the data here, but I'm not interested in that right now." Conversely, if they are, they can go to that function and give it their full attention, without all the other distractions. SRP is beginning to shape here.

Our PickerPage file now looks less cluttered.

import React, { useState } from "react";
import useSWR from "swr";

import { endpoints, DoggoBreedPicResponse } from "../api/";
import { useDoggoData } from "./useDoggoData";

import styles from "../pickerPage.module.css";

const NO_SELECTION = "";

export const PickerPage = () => {
  const doggos = useDoggoData();

  const [currentDoggoBreedId, setCurrentDoggo] = useState(NO_SELECTION);

  const { data: doggoPictureResponse, revalidate } = useSWR<
    DoggoBreedPicResponse
  >(
    // If this function returns something falsy, useSWR won't make a request.
    () => currentDoggoBreedId && endpoints.pictureForBreed(currentDoggoBreedId)
  );

  return (
    <div className={styles.container}>
      <span className={styles.header}>What's your favorite doggo breed?</span>
      {doggos && (
        <select
          value={currentDoggoBreedId}
          onChange={({ target: { value } }) => setCurrentDoggo(value)}
          className={styles.select}
        >
          {doggos.map(({ breedId, breedLabel }) => (
            <option key={breedId} value={breedId}>
              {breedLabel}
            </option>
          ))}
        </select>
      )}
      {doggoPictureResponse && (
        <>
          <img
            className={styles.image}
            src={doggoPictureResponse?.message}
            alt="Doggo's pic"
          />
          <button onClick={() => revalidate()}>Get a different picture!</button>
        </>
      )}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Let's see if we can do something about the remainder of the code in the render function.

const [currentDoggoBreedId, setCurrentDoggo] = useState(NO_SELECTION);

const { data: doggoPictureResponse, revalidate } = useSWR<
  DoggoBreedPicResponse
>(
  // If this function returns something falsy, useSWR won't make a request.
  () => currentDoggoBreedId && endpoints.pictureForBreed(currentDoggoBreedId)
);
Enter fullscreen mode Exit fullscreen mode

This code is sort of coupled, because the second part of it really depends on the useState part. So I vote we put it into one custom hook (what should go into which hook and how granular you should be is a topic for itself, and honestly, probably the hardest part in all of this).

We can make a new hook and call it useCurrentDoggo

import { useState } from "react";
import useSWR from "swr";

import { endpoints, DoggoBreedPicResponse } from "../api/";
import { NO_SELECTION } from "../constan";

export const useCurrentDoggo = () => {
  const [currentDoggoBreedId, setCurrentDoggoBreedId] = useState(NO_SELECTION);

  const { data: doggoPictureResponse, revalidate } = useSWR<
    DoggoBreedPicResponse
  >(
    // If this function returns something falsy, useSWR won't make a request.
    () => currentDoggoBreedId && endpoints.pictureForBreed(currentDoggoBreedId)
  );

  const currentDogoPictureUrl = doggoPictureResponse?.message;

  return {
    currentDoggoBreedId,
    setCurrentDoggoBreedId,
    currentDogoPictureUrl,
    fetchNewDoggoPicture: revalidate
  };
};
Enter fullscreen mode Exit fullscreen mode

Notice how we are returning an object filled with data that our component needs. It's very tailor-made for it. And notice how we can give more descriptive names to some variables so that our hook actually becomes an API that our component can use. For example, we have renamed revalidate to fetchNewDoggoPicture, which is a lot more descriptive about what the function does! Not only that, we could ditch SWR some day and change how that function works internally, but our component wouldn't have to care at all as long as the signature of the function is still the same. We have abstracted away how we do certain things and we just left our component with the API it needs to do its work, its own responsibility.

const doggos = useDoggoData();
const {
  currentDoggoBreedId,
  setCurrentDoggoBreedId,
  currentDogoPictureUrl,
  fetchNewDoggoPicture
} = useCurrentDoggo();
Enter fullscreen mode Exit fullscreen mode

This is all the code that's left in our new PickerPage now. That's it.

Let's recap on React Components. They are composable and therefore, you can extract a certain part of the JSX into its own component and make it many times more readable and less susceptible to user error (by enforcing good Prop interfaces).

There's no reason why hooks couldn't be the same! I hope this example demonstrates that nicely.

Feel free to take a look at the final look of the component.

Notice how our imports got a lot simpler. No dependencies on useMemo, useState. That's because the component never had to depend on those. All that's left do do now is simplify the JSX part of the component, but I'll leave that part up to you, as it's not in the scope of this article.

Feel free explore the example, too, just open the sandbox. I'm sure you'll be amazed by its complexity. 😄

Should you always make custom hooks for every little thing?

The answer is no. As always, you'll need to find a balance, and that balance will depend on you, your team and the project you're working on. And I know, I know... You probably hate that answer, but that's the reality in which we live in. We as developers have this great task of estimating how much will we need to engineer something so that it's neither under or over engineered for our current needs.

To give a concrete example: if you're just using an useState somewhere, definitely do not put that in its own hook. Also, if you're using SWR or React Query and the data you get back from the backend already perfectly fits your component's needs, there's no need to extract a custom hook either.

What about testing?

Well, as Kent C Dodds says, you should always test your components as a whole, the way you'd use them, so it shouldn't matter where your code lives. If your component is complex, you could find ways to interact with it directly as explained in the article.


And that's it! I hope this helped some of you in some way. Have a great day! 👋

Latest comments (4)

Collapse
 
trebuhd profile image
Hubert Dworczyński

Awesome article! I always do this wherever possible - it's a great way to decouple logic from the presentational layer. Since you can just mock your hook's return values, testing also becomes a breeze.

Collapse
 
milos192 profile image
Miloš Šikić

Thanks, man! Glad you liked it!

Collapse
 
cili93 profile image
Aleksandar Ilic

Great article Milos!

bleki #bestdoggo

Collapse
 
milos192 profile image
Miloš Šikić

Thank you very much, Aleksandar!

bleki #bestdoggo indeed