DEV Community

Eric
Eric

Posted on

I Made a Hook!

Introduction

In 2021, you can't learn React without learning about Hooks. According to the authority, React:

Hooks are functions that let you “hook into' React state and lifecycle features from function components. ...A custom Hook is a JavaScript function whose name starts with ‘use’ and that may call other Hooks.

In other words, Hooks are functions that are able to use React features and syntax, most notably, the built-in Hooks useState & useEffect.

In my VERY early journey into learning React, Hooks have been useful to me for separating complex or distracting logic from React components into their own files/functions, which cleans up my components. I like this because it makes the more important logic standout and my components easier to comprehend overall. Refactoring custom Hooks into their own files also makes them reusable throughout my entire application. As my applications grow, this versatility will become more and more critical in keeping my code DRY and easy to comprehend.

One almost unavoidable task when building an app is requesting data with a fetch request. For my second of five projects in my coding bootcamp, I’m building a restaurant point-of-sales (POS) application. Important setup data, like a menu or a list of saved customers, are stored in a json file that requires a fetch request to retrieve and use. For this article, however, I'll be using the POKEAPI in my examples, if only to make them easier to follow. You can check out my restaurant POS here if you'd like.

First, I'll quickly explain how I make basic fetch requests in React, without using any custom Hooks. After that, I'll demonstrate how I created a custom Hook to do the same thing, as well as, go over their differences.


Making a basic fetch request (in React)

Below is an example of how to make a fetch request inside a React component and store the results/error within a piece of state.

import { useState, useEffect } from 'react';

const POKEMON_URL = 'https://pokeapi.co/api/v2/pokemon';

export default function App() {
  const [pokemon, setPokemon] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    setIsLoading(true);
    fetch(POKEMON_URL)
      .then(res => res.json())
      .then(pokeData => setPokemon(pokeData))
      .catch(err => {
        console.error(err);
        setError(err);
      })
      .finally(() => setIsLoading(false));
  }, []);

  // Example usage of pokemon data
  const pokemonCards = pokemon.results.map(poke => {
    // function logic...
  })

  return (
    // Conditional JSX template...
  );
}
Enter fullscreen mode Exit fullscreen mode

I initiated three separate pieces of state to handle the fetch call:

  • pokemon – stores the value of a successful fetch request,
  • error – contains any errors that may occur,
  • isLoading – a boolean indicating whether or not a fetch request is currently taking place.

If the request is successful, the response is stored in results, otherwise, I have a catch function that stores the error in error, if the fetch fails. After the fetch is resolved, regardless of the results, I need to set isLoading back to false, which is handled inside finally() Since fetching data is asynchronous, meaning it takes some time to complete, isLoading gets set to true until the fetch either succeeds or fails, and is false while nothing is being fetched. Now, I can use the pokemon data I've fetched in the rest of my application just like any other piece of state.

As mentioned above, I find custom hooks useful for separating the logic inside my components into their own functions and/or files, leaving only the essence of what my component is intended for. This lightens up my component and makes comprehending it a lot easier. Also, my useFetch Hook can now be reused by any component, giving my fetch call more utility, as it’s no longer confined to only one component.


My custom Hook - useFetch

The block of code below does exactly the same thing as the previous code block, only the fetch logic is entirely inside of my useFetch Hook (function), including my three state variables. I've also added an AbortController to my Hook to "cleanup" my useEffect Hook.

App.js

import { useState, useEffect } from 'react';

const POKEMON_URL = 'https://pokeapi.co/api/v2/pokemon';

const useFetch = () => {
  const [pokemon, setPokemon] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    const { signal } = controller;
    setIsLoading(true);
    fetch(POKEMON_URL, { signal })
      .then(res => res.json())
      .then(pokeData => setPokemon(pokeData))
      .catch(err => setError(err))
      .finally(() => setIsLoading(false));
    return () => controller.abort();
  }, []);
  return { pokemon, isLoading, error };
};

export default function App() {
  const { pokemon, isLoading, error } = useFetch();

  // Example usage of pokemon data
  const pokemonCards = pokemon.results.map(poke => {
    // function logic...
  })

  return (
    // conditional JSX template...
  );
}
Enter fullscreen mode Exit fullscreen mode

Moving my fetch logic into it's own custom Hook makes my component easier to understand. I think it's pretty obvious that useFetch is a function that makes a fetch call. Code that's easy to read is highly encouraged, from what I've gathered, and it makes sense, especially when collaborating with others. I hate when I have to reread my old code a few times to understand what it does.

Note: it’s always best for custom Hooks to start with the word ‘use’ followed by whatever you want (useFetch, useState, useEffect). This is to let other programmers and react know right away that the function is a react Hook. I'm probably oversimplifying it, so if you want to know more, you can check out the docs for React, here.


Refactoring into useFetch.js

Technically, I did it. I made a custom Hook. It doesn't need to be refactored into a separate file to be a custom Hook (I only say that because that was my impression, at first), but doing so but doing so has a couple advantages for me. First, it will make my component MUCH cleaner and, more importantly, I can make it even more reusable, allowing me to import it into any custom Hook or component.


App.js

import useFetch from '../hooks/useFetch';

const POKEMON_URL = 'https://pokeapi.co/api/v2/pokemon';

export default function App() {
  const {results: pokemon, isLoading, error} = useFetch(POKEMON_URL);

  // Example usage of pokemon data
  const pokemonCards = pokemon.results.map(poke => {
    // function logic...
  })

  return (
    // conditional JSX template...
  );
}
Enter fullscreen mode Exit fullscreen mode

useFetch.js

import { useState, useEffect } from 'react';

export default function useFetch(url) {
  const [results, setResults] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    setIsLoading(true);
    const controller = new AbortController();
    const { signal } = controller;
    fetch(url, { signal })
      .then(res => res.json())
      .then(data => setResults(data))
      .catch(err => setError(err))
      .finally(() => setIsLoading(false));
    return () => controller.abort();
  }, [url]);

  return { results, isLoading, error };
}
Enter fullscreen mode Exit fullscreen mode

I think that looks WAY cleaner and makes reading it MUCH easier. This way, I'm able to get rid of all of the useFetch logic in App.js, including my three state variables, as well as, my useState & useFetch imports. All I have to do is import useFetch at the top and invoke it, destructuring the my three state variables from it's return. Notice I've changed the name of the 'pokemon' state variable to 'results', to make it more universal. Notice I did, however, rename it to 'pokemon' when destructuring it inside App.js.

The logic inside of useFetch.js is basically cut right from App.js. The only difference is I've made it more dynamic by creating a 'url' parameter so I can use my Hook to call other endpoints, if necessary.

When creating custom Hooks, I always store them inside a folder called hooks, located just inside the src folder (/src/hooks/useFetch.js). This is the most common way to structure your hook files, from my experience.

Folder structure

It’s also best-practice for the file name and the Hook name to be the same. Also, you should export the function by default.


Conditional rendering

Once I've destructured my state variables from useFetch(), I can use them to conditionally output JSX based on the their current values.

From my App.js examples above...

  return (
    // conditional JSX template...
  );
Enter fullscreen mode Exit fullscreen mode

This portion of code is commented out to make my examples shorter and less overwhelming, but now I'll open it up and show one way to use state to conditionally output JSX.

Hopefully, your familiar with conditional rendering, in general, but if not, you can learn more about it here.

return (
  <>
    {pokemon && !isLoading && (
      <div className="FetchHook">
        {pokemon.results.map((poke, i) => (
          <div key={i + 1}>{poke.name}</div>
        ))}
      </div>
    )}
    {isLoading && <div>Loading...</div>}
    {error && !isLoading && (
      {console.error(error)}
      <div>
        <h2>ERROR:</h2>
        <p>{JSON.stringify(error)}</p>
      </div>
    )}
  </>
);
Enter fullscreen mode Exit fullscreen mode

Above, there's three possible templates being rendered, but none at the same time. First, I checked if the pokemon variable was truthy, meaning it wasn't empty, and I also made sure isLoading was set to false, meaning the fetch request was resolved. Currently, I'm simply outputting each pokemon's name inside a div, but if I were to take this further and make a pokedex, I could map through the results to create an interactive list of pokemon cards or whatever.

Second, whenever isLoading is true, I wanted to output a message saying so. This is where a loading spinner or a skeleton could be rendered.

And third, if errors is ever true, meaning there was an error while fetching the data, I wanted to output that information to let the user know, as well as, make sure isLoading was set back to false.

To bring this article full circle, because I extracted useEffect into it's own file, the rest of my App component can focus on it's intended purpose, which is to output a list of pokemon, making the code easier to comprehend on the first read-through.


More info on Hooks

Unlike React components, Hooks can have any return value, not just valid JSX. For example, useState returns a state variable and a function to set it, that is destructured from calling useState(). useEffect, on the other hand, doesn't return anything.

By definition (see above), Hooks can call other Hooks, including useState, meaning Hooks can initialize and manipulate state. That's part of what make custom Hooks so powerful! This also means that my three state variables can only be updated from within useFetch.js, unless I include their set functions in the return. There should never be a reason for me to update these variables from outside the file it lives in besides refetching the data, but if there ever is one, I can always go into useFetch.js and simply export whichever set function I need.


Conclusion

So with that... I made a Hook! If you want to know more about Hooks, custom Hooks, or React in general, the best place is at reactjs.org.

There were a few videos on YouTube that really helped me understand making custom Hooks, specifically making a custom useFetch Hook. In general, I owe these guys a lot for teaching me everything:

If you've made it this far, thank you, sincerely, for reading my article. This is only the third blog post I've ever made, so if I've made any huge mistakes or you want to give me advice on how to do this better, please, let me know!

That's it.

- END -

Top comments (0)