React custom hooks

·

4 min read

Custom hooks were something that confused me for a long time. I could use useState happily, but didn't see why you'd use a custom hook or how you'd create one. Until this week.

The problem

My app includes a component (Search) that has an input box and a search button. When you click the search button it fetches data from an API (in a function called fetchData) and displays that data in another component (Cards). Additionally, in another component (Button) there's a button that you can click to get more results. Since my API only returns 10 results at a time, this button runs the same API call as Search did, just with a different parameter to tell it to start at the 11th item. All of these components are children of App.

Originally it made sense to me, coming from JavaScript, to have the fetchData function in a separate file. Except that Cards needs the data I get from fetchData. The easiest way to do that is to store data in a state hook. But you can't set the state in an ordinary function - as the React docs tell us. I also found that my function just ran when I opened my app, which is not what I wanted.

The (not great) solution - put everything in App

You could solve this by having everything in App and no child components at all. But that is unwieldy and one of the points of having components is that each one just does one thing.

The other solution to that is to put fetchData in App and then pass the function to the Search and Button components and the data to the Cards component. Which works, but you still have a lot going on in App. Which I thought was fine until I tried to explain what I had done to someone else and realised I couldn't find anything.

Custom hooks to the rescue

The custom hook contains the setup for my state hook that holds the data, and the fetchData function. It then returns both of those, which I can pass down to the child components, the same as if they lived on App.

Here's an example.

App:

import Search from './components/Search';
import Cards from './components/Cards';
import Button from './components/Button';
import useAPI from './hooks/useAPI';

const App = () => {
  const { fetchData, results } = useAPI();

  return (
    <>
      <Search fetchData={fetchData} />
      <Card results={results} />
      <Button fetchData={fetchData} />
    </>
  )
}

Everything's pretty standard here, we import the components and the custom hook, define everything, send things to the components. The interesting bit is the custom hook.

If you had a state hook you'd define it like this:

const [results, setResults] = useState();

The custom hook is very similar. But fetchData is a function, results is a state hook. I don't need to define setResults because I'm not using it in App.

Since all hooks start with 'use', so do custom hooks.

useAPI:

import { useState } from 'react';

const useAPI = () => {
  const [results, setResults] = useState();

  const fetchData = async () => {
    //get data from API as usual
    setResults((arr) => [...arr, response.data]);
  } 

 return {
    fetchData,
    results,
  }

}
export default useAPI;

So here we define our state hook as usual. We need both results and setResults because we set it in fetchData and return results back to App, so we can pass it to Cards. All you have to return is everything you need in App (or child components).

That's it! It really is that simple.

Except...

You have to think of your custom hook the same way you do a state hook. So you define it where you need it, and if any child components need it, you pass it down. You can re-define it in your child component. Or define the bits you need in the parent and the bits you need in the child. It then won't work. As I found out.

It's usefulness is in getting the code separated out, so everything is doing one thing, not in being able to use it everywhere.