"People think that computer science is the art of geniuses but the actual reality is the opposite, just many people doing things that build on each other, like a wall of mini stones" -- Donald Knuth


React Hooks have changed the way we write components. Hooks have mentally pushed us to write more Functional than Classical Components.

Though once you start making your app with Hooks, suddenly you have 10s of different hooks and even though they are managing a related state, it becomes hard to manage them.

They start feeling like clutter in the good-old Functional Components.

Looks unrelated? Have a look at this Component:

import React from "react";

const DataTable = ({ movies = [] }) => (
  <div>
    {movies.map(({ Poster, Title, imdbID, Year }) => (
      <div key={imdbID}>
        <img src={Poster} height="100" alt={Title} />
        <p>
          <a href={`/?t=${imdbID}`}>{Title}</a>
          <br />
          {Year}
        </p>
      </div>
    ))}
  </div>
);

export default DataTable;


Now if we were to add the Data Loading requests and building the profile links, there are two ways to do it:

1 - Add all the requests and function in the same component

2 - Create a wrapper component to:

  • Make a request and build links
  • Pass all the required Data and Functions as Props

Let’s try to see both ways and how our computer develops in size and functionalities.


Loading Data, building event handlers and Markup in the same component:

import React, { useEffect, useState, useContext } from "react";
import KEY from "./KeyContext";

const url = "http://www.omdbapi.com/?s=";

const DataTable = ({ query = "Harry Potter" }) => {
  const key = useContext(KEY);
  const [movies, setMovies] = useState([]);
  useEffect(() => {
    fetch(`${url}${query}&apikey=${key}`)
      .then((r) => r.json())
      .then((res) => setMovies(res.Search.sort((a, b) => a.Year - b.Year)));
  }, [key, query]);

  return (
    <div>
      {movies.map(({ Poster, Title, imdbID, Year }) => (
        <div key={imdbID}>
          <img src={Poster} height="100" alt={Title} />
          <p>
            <a href={`/?t=${imdbID}`}>{Title}</a>
            <br />
            {Year}
          </p>
        </div>
      ))}
    </div>
  );
};

export default DataTable;


And if we make a Wrapper Component to wrap the data table and pass the data as props; it will look like the following:

import React, { useEffect, useState, useContext } from "react";
import KEY from "./KeyContext";

const url = "http://www.omdbapi.com/?s=";

const DataTable = ({ movies = [] }) => (
  <div>
    {movies.map(({ Poster, Title, imdbID, Year }) => (
      <div key={imdbID}>
        <img src={Poster} height="100" alt={Title} />
        <p>
          <a href={`/?t=${imdbID}`}>{Title}</a>
          <br />
          {Year}
        </p>
      </div>
    ))}
  </div>
);

const DataContainer = ({ query = "Harry Potter" }) => {
  const key = useContext(KEY);
  const [movies, setMovies] = useState([]);
  useEffect(() => {
    fetch(`${url}${query}&apikey=${key}`)
      .then((r) => r.json())
      .then((res) => setMovies(res.Search.sort((a, b) => a.Year - b.Year)));
  }, [key, query]);

  return <DataTable movies={movies} />;
};

export default DataContainer;


Now here comes the custom hooks.

As we saw firstly, we can take the loading of data and related functions in separate functions which will trigger the same thing through that function.

Additionally, we can have a context to initialize the defaults and some common data to share among apps

First of all, we want to separate the data loading. Let's make out new hook called useMovies

const useMovies = (query = null) => {
  return fetch(`${url}${query}&apikey=${key}`)
    .then((r) => r.json())
    .then((r) => r.Search.sort((a, b) => a.Year - b.Year));
};

Now that our function is doing data loading, let's add some persistence to it with state hooks:

import { useState } from "react";

const useMovies = (query = null) => {
  const [movies, setMovies] = useState([]);
  fetch(`${url}${query}&apikey=${key}`)
    .then((r) => r.json())
    .then((r) => r.Search.sort((a, b) => a.Year - b.Year))
    .then(setMovies);
  return movies;
};

But we want to load the movies on the first call, not every call; and then get the new data when there is a change in the query.

Along with that, let’s separate the fetching/AJAX code in a separate file.

With above mentioned separation of concerns in the code; we have the following useMovies hook and request module respectively:

// useMovies.js
import { useState, useEffect, useContext } from "react";
import KeyContext from "./KeyContext";
import request from "./request";
import queryString from "query-string";

const url = "http://www.omdbapi.com/";

const sortMovies = (movies = []) => movies.sort((a, b) => a.Year - b.Year);

const getUrl = (params) => [url, queryString.stringify(params)].join("?");

const useMovies = (query = null) => {
  const [q, setQuery] = useState(query);
  const [movies, setMovies] = useState([]);
  const apikey = useContext(KeyContext);

  useEffect(() => {
    q &&
      request(getUrl({ apikey, s: q }))
        .then((r) => r.Search)
        .then(sortMovies)
        .then(setMovies);
  }, [q, apikey]);

  return [movies, setQuery];
};

export default useMovies;

// request.js
export default (url, params) =>
  fetch(url, params).then((response) => {
    if (response.status === 200) {
      try {
        return response.json();
      } catch (e) {
        return response.text();
      }
    }
    return response;
  });

In the above function of our custom hook we did the following:

  • Receive the First Query & initialize a state to receive changes in Query
  • Movies data with useState Hook
  • API key from the Context and useContext Hook
  • Use useEffect to:

    1.Trigger the first request for First Query
    2.Request changes to API on change of Query
    3.As API Key is coming from Context, it is prone to change and hence keep it in the
    Dependency of useEffect hook
    4.Return the Data (i.e. movies) and Function to change Query (i.e. setQuery)


Though while creating or using hooks, there are two rules that you need to keep in mind

  • Only Call Hooks at the Top Level
  • Only Call Hooks from React Functions

Example with Firebase Auth

A very common scenario is you have a bunch of components that need to render different depending on whether the current user is logged in and sometimes call authentication methods like signin, signout, sendPasswordResetEmail, etc.

This is a perfect use-case for a useAuth hook that enables any component to get the current auth state and re-render if it changes. Rather than have each instance of the useAuth hook fetch the current user, the hook simply calls useContext to get the data from farther up in the component tree. The real magic happens in our <ProvideAuth>component and our useProvideAuth hook which wraps all our authentication methods (in this case we're using Firebase) and then uses React Context to make the current auth object available to all child components that call useAuth. Whew, that was a mouthfull...

Hopefully as you read through the code below it should all make sense. Another reason I like this method is it neatly abstracts away our actual auth provider (Firebase), making it super easy to change providers in the future.

useAuth

// Top level App component
import React from "react";
import { ProvideAuth } from "./use-auth.js";

function App(props) {
  return <ProvideAuth>{/* 
        Route components here, depending on how your app is structured.
        If using Next.js this would be /pages/_app.js
      */}</ProvideAuth>;
}

// Any component that wants auth state
import React from "react";
import { useAuth } from "./use-auth.js";

function Navbar(props) {
  // Get auth state and re-render anytime it changes
  const auth = useAuth();

  return (
    <NavbarContainer>
      <Logo />
      <Menu>
        <Link to="/about">About</Link>
        <Link to="/contact">Contact</Link>
        {auth.user ? (
          <Fragment>
            <Link to="/account">Account ({auth.user.email})</Link>
            <Button onClick={() => auth.signout()}>Signout</Button>
          </Fragment>
        ) : (
          <Link to="/signin">Signin</Link>
        )}
      </Menu>
    </NavbarContainer>
  );
}

// Hook (use-auth.js)
import React, { useState, useEffect, useContext, createContext } from "react";
import * as firebase from "firebase/app";
import "firebase/auth";

// Add your Firebase credentials
firebase.initializeApp({
  apiKey: "",
  authDomain: "",
  projectId: "",
  appID: "",
});

const authContext = createContext();

// Provider component that wraps your app and makes auth object ...
// ... available to any child component that calls useAuth().
export function ProvideAuth({ children }) {
  const auth = useProvideAuth();
  return <authContext.Provider value={auth}>{children}</authContext.Provider>;
}

// Hook for child components to get the auth object ...
// ... and re-render when it changes.
export const useAuth = () => {
  return useContext(authContext);
};

// Provider hook that creates auth object and handles state
function useProvideAuth() {
  const [user, setUser] = useState(null);

  // Wrap any Firebase methods we want to use making sure ...
  // ... to save the user to state.
  const signin = (email, password) => {
    return firebase
      .auth()
      .signInWithEmailAndPassword(email, password)
      .then((response) => {
        setUser(response.user);
        return response.user;
      });
  };

  const signup = (email, password) => {
    return firebase
      .auth()
      .createUserWithEmailAndPassword(email, password)
      .then((response) => {
        setUser(response.user);
        return response.user;
      });
  };

  const signout = () => {
    return firebase
      .auth()
      .signOut()
      .then(() => {
        setUser(false);
      });
  };

  const sendPasswordResetEmail = (email) => {
    return firebase
      .auth()
      .sendPasswordResetEmail(email)
      .then(() => {
        return true;
      });
  };

  const confirmPasswordReset = (code, password) => {
    return firebase
      .auth()
      .confirmPasswordReset(code, password)
      .then(() => {
        return true;
      });
  };

  // Subscribe to user on mount
  // Because this sets state in the callback it will cause any ...
  // ... component that utilizes this hook to re-render with the ...
  // ... latest auth object.
  useEffect(() => {
    const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
      if (user) {
        setUser(user);
      } else {
        setUser(false);
      }
    });

    // Cleanup subscription on unmount
    return () => unsubscribe();
  }, []);

  // Return the user object and auth methods
  return {
    user,
    signin,
    signup,
    signout,
    sendPasswordResetEmail,
    confirmPasswordReset,
  };
}

Custom hooks means fewer keystrokes and less repetitive code.