Hooks in React were introduced in React 16.8. Hooks became a hit and saw a lot of adoption. A lot of libraries in the ecosystem now provide hooks support. One of the main motivations for introducing hooks was to enable sharing of state logic across components. You can read more about this here.

In this post, I will go over how to write a custom hook with an example.

Problem

I was working with Supabase for one of my side projects and I wanted to abstract away the API calls made to Supabase from components. So I thought this would be a good opportunity to write a custom hook.

Here’s a list of requirements for the custom hook:

  • Send a request to the supabase endpoint.
  • Handle errors if any.
  • Return data from the request.

Pretty simple.

Solution

//useSupabase.js

import { useState, useEffect } from "react";

//supaCall calls the Supabase API
const useSupabase = (supaCall) => {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [data, setData] = useState(null);

  useEffect(() => {
    async function getData() {
      setLoading(true);
      try {
        let { data, error } = await supaCall();
        if (error) {
          setError(error);
          // Show toast with relevant error message to user
          // Log error to Airbrake or Sentry
        } else {
          setData(data);
        }
      } catch (e) {
        // Likely to be a network error
        setError(e);
      } finally {
        setLoading(false);
      }
    }
    getData();
  }, []);

  return { loading, data, error };
};

export default useSupabase;

useSupabase is a function that accepts a function as a parameter.

There are 3 state variables →

  • loading → Describes the state of the request. This can be used to show a loading icon.
  • error → captures errors if any.
  • data → Raw data returned by the API call.

Things to note

  • Global error handling can be done after or before the setError call. The error can be logged to Airbrake or similar tools. For better UX, a toast message with a relevant error message can be shown to the user.
  • The supaCall argument passed to the hook is a function. More on that later.
  • The Supabase API is called in the useEffect hook. So whenever the component (which uses this hook) is mounted the API call is triggered.

Usage

Let’s quickly take a look at how the custom hook is used.

//Posts.js
import useSupabase from "../hooks/useSupabase";
import { getAllPosts } from "../services/supaservice";

function Posts() {
  const { loading, data, error } = useSupabase(getAllPosts);

  return (
    <Flex flexDirection="column" mt="10">
      {loading && <Text>Loading...</Text>}
      {!loading && error && <Text>Error</Text>}
      {!loading && data && data.map((someProps) => <Post {...someProps} />)}
    </Flex>
  );
}

// services/supaservice.js
const getAllPosts = async () => {
  let { data, error } = await supabase.from("posts").select("*");
  return { data, error };
};

The getAllPosts method is passed as an argument to the useSupabase hook. The getAllPosts function calls the Supabase API (using the supabase client).

So the way it works is, the useSupabase hook gets called with the getAllPosts method, and when the component is mounted, the API call is triggered which returns all posts and gets rendered on the screen.

Usage with a parameter passed to the function

In many cases, the function passed to useSupabase would require an input. In that case, we can use bind and pass arguments.

import { getDisplayName } from "../services/supaservice";

function Profile({ id }) {
  const { loading, data, error } = useSupabase(getDisplayName.bind(this, id));

  return <Flex>{!loading && data && <Text>{data["display_name"]}</Text>}</Flex>;
}

export default Profile;

// services/supabaseservice.js

const getDisplayName = async (id) => {
  const { data, error } = await supabase
    .from("profiles")
    .select("display_name");
  return { data, error };
};

But wait..this isn’t good enough

There is a problem here though. There are cases where the API needs to be called after the user performs an action. For example, adding a new post after the user clicks on the submit button. For those cases, this hook isn’t suitable.

useSupabase with a callback

import { useState, useEffect } from "react";

const useSuapbaseWithCallback = (supaCall) => {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [data, setData] = useState(null);

  async function callService() {
    setLoading(true);
    try {
      let { data, error } = await supaCall(arguments);
      if (error) {
        setError(error);
      } else {
        setData(data);
      }
    } catch {
      setError(error);
    } finally {
      setLoading(false);
    }
  }
  return { callService, loading, data, error };
};

export default useSuapbaseWithCallback;

The change is subtle but important. Rather than the API getting called on the component mount, a callback is returned as one for the return values. This callback can then be used to call the API in event handlers.

In the code above, callService is returned as a callback. This is similar to the useMutation hook from the Apollo GraphQL library.

Usage (useSupabaseWithCallback)

import { updateDisplayName } from "../services/supaservice";
import useSuapbaseWithCallback from "../hooks/useSuapbaseWithCallback";

function Account() {
  const [newDisplayName, setNewDisplayName] = useState(null);

  const { callService, loading, data, error } =
    useSuapbaseWithCallback(updateDisplayName);

  const updateName = async () => {
    if (newDisplayName) {
      await callService(newDisplayName, user.id);
    }
  };

  return (
    <Flex flexDirection="column">
      .....
      <Button onClick={updateName}>Update</Button>
      ....
    </Flex>
  );
}

export default Account;

// services/supabaseservice.js

const updateDisplayName = async ([display_name, id]) => {
  const { data, error } = await supabase
    .from("profiles")
    .update({ display_name })
    .match({ id });
  return { data, error };
};

The callService is a callback returned by useSuapbaseWithCallback hook. This callback can be called after a user performs a certain action. In the code above, the callback is called after the user clicks on the update button.

And that’s how you can write custom hooks in react. Remember to start the name of your custom hook with use so that it gets marked if its usage violates the rules for hooks.

If you liked this blog post you might also like,

👉 Writing a useState hook from scratch

👉 OAuth2 with Reddit API

I am on twitter as rahulnpadalkar. I sometimes post memes and hot takes there.

I also make Youtube videos.

Some of my most watched videos

🔗 Event Delegation in Javascript

🔗 Writing a custom middleware in Express