#16 The magic of custom hooks - Delightful React

#16 The magic of custom hooks - Delightful React

We talked about a couple of hooks so far useState and useEffect in our previous chapters. But the best is yet to come!

Asset_61.png

*Note: This article is a part of the Delightful React Series, as part of which, I am releasing one chapter every day for 25 days. Please support me in anyway you can! *


Hooks are just functions, aren't they?

Isn't this how a useState works?

const stuff = useState(5);

If hooks are just functions, then won't all the flexibility of functions apply to hooks? In Javascript, a function is simply a way to group code. And you can also group multiple functions together, can't you?

Asset_62.png

Aren't these two code samples identical in behaviour?

  • Code sample 1

const a = 1
function printA(){
   console.log(a)
}
a();
  • Code sample 2

function doWork(){
  const a = 1
  function printA(){
     console.log(a)
  }
  a();
}

doWork();

They absolutely are. Even though printA is inside a function doWork, it's order of execution or the way it is eventually used hasn't changed all that much.

Similarly, we can do the same thing with hooks

function MyComponent(){
   const [value, setValue] = useState(5)
   return <p>{value} </p>
}

vs


function doWork(){
    return useState(5)
}

function MyComponent(){
   const [value, setValue] = doWork()
   return <p>{value} </p>
}

All we did is simply group our hook usage into another function and we called that function instead. Such functions which internally calls other hooks is called a custom hook and custom hooks help us take composition to the next level in React.

Asset_64.png

Compose hooks into custom hooks

Custom hooks help us group or compose hooks into reusable functions. We can compose a simple useState hook like so.


function doWork(){
    return useState(5)
}

function MyComponent(){
   const [value, setValue] = doWork()
   return <p>{value} </p>
}
  • Except, the rules of hooks also apply to custom hooks.

In other words, even though we group multiple hooks together, they will still execute in the top level of the component and all the rules of hooks are still satisfied.

Artboard_Copy_44.png

  • So we should still name this hook starting with the word use and we can't use this hook anywhere else except the top level and we also can't use it within conditional statements and loops etc which may cause the count of the hooks to change across renders.

  • So a basic custom hook on useState can look like this.


function useDoWork(){
    return useState(5)
}

function MyComponent(){
   const [value, setValue] = useDoWork()
   return <p>{value} </p>
}
  • Well this hook isn't that useful. It just added a few more lines to a very simple use case. So don't just make custom hooks that don't add any value.

  • In fact, custom hooks are useful when they solve a simple purpose in reusable way. For eg, in our blogpost page we had a component show up or hide itself when a button was clicked. This is a simple toggle behaviour that is extremely common in frontend.

  • So let's try making a custom hook out of it.

Code starting point for the chapter

Feel free to use this codesandbox as the starting point for this chapter and you can follow along the instructions in the chapter.

Let's start small

We have, in our blogpost page, a component Post that looks like this.

//pages/blogpost.js

function Post() {
  const [showCommentUI, setShowCommentUI] = useState(false);
  return (
    <div>
      <PostBody />
      <button
        onClick={function () {
          setShowCommentUI(!showCommentUI);
        }}
      >
        {showCommentUI ? "Hide Comment Form" : "Add A Comment"}
      </button>
      {showCommentUI ? <AddComment /> : null}
      <br />
      <br />
      <br />
    </div>
  );
}

So let's try to create a naive custom hook where we just move all our useState and the toggle behaviour into that hook.

function useShowCommentUI(){
  const [showCommentUI, setShowCommentUI] = useState(false);

  function toggle(){
    setShowCommentUI(!showCommentUI);
  }

  return [showCommentUI, toggle]
}

function Post() {
  const [showCommentUI, toggle] = useShowCommentUI();
  return (
    <div>
      <PostBody />
      <button
        onClick={toggle}
      >
        {showCommentUI ? "Hide Comment Form" : "Add A Comment"}
      </button>
      {showCommentUI ? <AddComment /> : null}
      <br />
      <br />
      <br />
    </div>
  );
}

Our code still works perfectly fine. Well it should, because we only rearranged things here and there. Everything else is still the same.

Let's make this hook reusable

  • useShowCommentUI is a very specific name and also does something very limited because it always starts off with a false value.

  • However, we can make this a more generic hook with a generic name that can simply start off with either true or false and toggles that value using the toggle function. Let's do that now.

function useToggle(initialValue = false){
  const [showCommentUI, setShowCommentUI] = useState(initialValue);

  function toggle(){
    setShowCommentUI(!showCommentUI);
  }

  return [showCommentUI, toggle]
}

function Post() {
  const [showCommentUI, toggle] = useToggle();
  return (
    <div>
      <PostBody />
      <button
        onClick={toggle}
      >
        {showCommentUI ? "Hide Comment Form" : "Add A Comment"}
      </button>
      {showCommentUI ? <AddComment /> : null}
      <br />
      <br />
      <br />
    </div>
  );
}
  • Of course we can still name our state variable as showCommentUI in the Post component but our useToggle hook is completely generic and reusable.
  • Let's keep all of our hooks in the src/hooks folder. So let's move this into src/hooks/useToggle.js file and import it into our blogpost page like so.

  • Move the hook into src/hooks/useToggle.js

//src/hooks/useToggle.js
import { useState } from "react";

export default function useToggle(initialValue = false) {
  const [showCommentUI, setShowCommentUI] = useState(initialValue);
  function toggle() {
    setShowCommentUI(!showCommentUI);
  }
  return [showCommentUI, toggle];
}
  • Use it in our page component.
//pages/blogpost.js 
import Heading from "../src/components/Heading";
import useToggle from "../src/hooks/useToggle";

function AddComment() {//...}

function PostBody() { //...}

function Post() {
  const [showCommentUI, toggle] = useToggle();
  return (
    <div>
      <PostBody />
      <button onClick={toggle}>
        {showCommentUI ? "Hide Comment Form" : "Add A Comment"}
      </button>
      {showCommentUI ? <AddComment /> : null}
      <br />
      <br />
      <br />
    </div>
  );
}

export default Post;

Custom hooks are powerful

We are just getting started but custom hooks are vert powerful.

  • Our useToggle hook only uses a single useState built in hook but imagine a custom hook that holds multiple useStates, multiple useEffects or multiple other built-in React hooks which you can drop into any component and suddenly make it very very powerful.

Anatomy_of_that_new_hook_you_want_to_build.png

This is what custom hooks bring to the table. A mixin like plug-and-play usage yet still doesn't look out of place. Simple functions that are very powerful within components and can make our lives easier.

Now, let's take this a step further and build 3 more hooks from our components we already built.

Also, we need to make a conscious step to build hooks independently from components. Which is why from now onwards, let's build custom hooks and build them by planning their behaviour

We can plan their behaviour by asking ourselves 3 questions

  • What are the arguments that this custom hook requires?
  • What should this hook return?
  • What other hooks should this hook use internally?

Artboard_Copy_43-1.png

If we can answer these questions, we can build our hooks very easily.

useInputState hook

We need state to control our input components and access the value of the input components as they render. So let's create a useInputState hook that can do this for us.

Let's ask ourselves the three magical questions.

Q1) What are the arguments that this custom hook requires? We are going to be dealing with a the value of input element with this hook. So we probably want to send the initial text that the state variable starts off with as an argument.

Q2) What should this hook return? To control an input, we only need value prop of the input and the onChange prop of the input to be set. If we get these two values from the hook we can apply them directly to the input as props. We basically need props to send to the input.

Q3) What other hooks should this hook use internally? We need a single state variable. So just one useState.

Nice. Now that we have this planned, let's build our useInputState hook at src/hooks/useInputState.js like so.

//src/hooks/useInputState.js
import { useState } from "react";

export default function useInputState(initialValue = "") {
  const [value, setValue] = useState(initialValue);

  function onChange(event) {
    setValue(event.target.value);
  }

  const inputProps = {
    value,
    onChange,
  };
  return inputProps;
}

That's it! Now we can apply this hook in our pages/index.js file like so.

Earlier we had this.

//pages/index.js
const [searchText, setSearchText] = useState("");

Now we do.

//pages/index.js
//...
import useInputState from "../src/hooks/useInputState";

function AllPosts() {
  const searchInputProps = useInputState("");
  const searchText = searchInputProps.value;
  //...

  return (
    <div>
      <div className="search-posts">
        <input
          type="text"
          value={searchInputProps.value}
          onChange={searchInputProps.onChange}
        />
       ...
     </div>
}

There is a very nice shortcut to pass all the contents of an object as props to an element/component in React. Notice how we are trying to pass value prop as searchInputProps.value and onChange as searchInputProps.onChange. There is a much shorter syntax which is

//pages/index.js
<input type="text" {...searchInputProps} />;

This is called spreading. We are simply spreading the contents of the object searchInputProps as props to our input element.

useDocumentTitle

Let's pick up the pace and built a useDocumentTitle hook which simply updates the title of the document with a value.

Let's answer the three questions for this hook again.

Q1) What are the arguments that this custom hook requires? The value to set as document.title.

Q2) What should this hook return? Nothing actually. This hook doesn't have to return anything.

Q3) What other hooks should this hook use internally? We only want to run a side-effect when the value changes. So we only need useEffect hook.

  • Let's get to work!
//src/hooks/useDocumentTitle.js
import { useEffect } from "react";

export default function useDocumentTitle(textToShow){
  useEffect(() => {
    document.title = textToShow;
  },[textToShow]);
}
  • Let's import into pages/index.js and use it.
//pages/index.js
//...
import useDocumentTitle from "../src/hooks/useDocumentTitle";

function AllPosts() {
  const searchInputProps = useInputState("");
  const searchText = searchInputProps.value;
  useDocumentTitle(searchText);
// ... 
}

useCheckboxState

Finally, let's build a useCheckboxState hook which is very identical to the useInputState, it just uses a different prop checked instead of value. So let's build that at src/hooks/useCheckboxState like so.

//src/hooks/useCheckboxState.js

import { useState } from "react";

export default function useCheckboxState(initialValue=false){
    const [value, setValue] = useState(initialValue)

    function onChange(event){
        setValue(event.target.checked)
    }

    const checkboxInputProps = {
        value,
        onChange
    }
    return checkboxInputProps
}
  • Now, let's import and use it like so.
//pages/index.js
//...
import useCheckboxState from "../src/hooks/useCheckboxState";
import useInputState from "../src/hooks/useInputState";
import useDocumentTitle from "../src/hooks/useInputState";

function AllPosts() {
  const searchInputProps = useInputState("");
  const searchText = searchInputProps.value;
  useDocumentTitle(searchText);

  const featuredCheckboxProps = useCheckboxState(false);
  const showFeaturedOnly = featuredCheckboxProps.value;


 return <div>
             ...
            <input id="is-featured" type="checkbox" {...featuredCheckboxProps} />
           ...
        </div>

Hooks and class components are similar than we think

The newer functional components with hooks syntax looks a whole lot different from the class component syntax. Even though class components aren't as favoured today, they still use the same underlying fabric of React which is Fiber.

  • Class components and Functional components with hooks internally still work very similarly
  • They have props, state and can compose other components.
  • They run side-effects in places separate from the "render" part of the code.
  • They just look very different but do the same things.

Rendering_lists_in_React_Copy_2.png

  • componentDidMount, shouldComponentUpdate, componentWillUnmount all have equivalent alternatives in hooks too. Although we haven't discussed all of them yet in our book. We will shortly.

Anyway, functional components and class components aren't that different however, hooks offer significantly higher composition capabilities because of how simply we can group multiple functions together vs group multiple methods or properties of a class.

Hooks are so much more easier to work with simply because of two things

  • Functions are so much easier to modularise than parts of a class in Javascript
  • React Hooks are able to access Fiber without even mentioning it anywhere in our codebase. This makes them super easy to move around and compose.
    • We never did something like import {Fiber} from 'react'. It's so automatic that hooks can be written anywhere and they work well in whichever component they are used.

That's it! We did quite a bit in this chapter. We learnt about custom hooks and also built few of our own today.

Codesandbox with custom hooks

Take a look at the finished codesandbox for this chapter here.

Thanks and Please support my work

  • Writing blog posts and making videos is an effort that takes many hours in my day. I do it because I love teaching and make great content.
  • However, I need your support too. Please support my work and follow my social accounts to help me continue to make great content.
  • Here are my social links.

Follow me on Twiter

Subscribe to my channel on Youtube

Thank you!