DEV Community

Cover image for Custom React Hooks: useBoolean
Ludal 🚀
Ludal 🚀

Posted on • Edited on • Originally published at iamludal.fr

Custom React Hooks: useBoolean

React hooks initially allow you to "hook into" React state and lifecycle features, like we used to do with the componentDidMount or componentWillUnmount methods when using class based components. What we'll discover in this article is that we can implement our own custom hooks, using the few primitives hooks React provides us, like useState and useEffect. This way, you can drastically reduce the cognitive complexity of your components, by moving away some logic into functions that you will be able to reuse anywhere in the other components of your React applications. Your code will look cleaner, and you're following the Single Responsibility Principle (SRP), which states that each class or function (or, in our case, component) should have responsibility over a single part of a program's functionality, and it should encapsulate that part.

Enough talk, let's get to work and implement our first custom hook: useBoolean! 😎

Motivation

First of all, why are we going to implement such a hook? Let's have a look at this simple component:



const Spoil = ({ content }) => {
  const [showSpoil, setShowSpoil] = useState(false);

  return (
    <div className="spoil">
      <button onClick={() => setShowSpoil((visible) => !visible)}>
        {showSpoil ? "Hide" : "Show"}
      </button>
      {showSpoil && <div className="spoil-content">{content}</div>}
    </div>
  );
};


Enter fullscreen mode Exit fullscreen mode

The component receives a content prop, that only appears once the button gets clicked to show the spoil. Of course, clicking the button again will hide it back, and so on.

Here, the component is so simple that it is very easy to read, but we could improve its readability by extracting the button onClick listener to a separate function:



const Spoil = ({ content }) => {
  const [showSpoil, setShowSpoil] = useState(false);

  const toggle = () => setShowSpoil((visible) => !visible)

  return (
    <div className="spoil">
      <button onClick={toggle}>
        {showSpoil ? "Hide" : "Show"}
      </button>
      {showSpoil && <div className="spoil-content">{content}</div>}
    </div>
  );
};


Enter fullscreen mode Exit fullscreen mode

That's better. We've simplified the return value of our function, but we've added a new line between the state initialization and the return statement. Of course this is not a major problem in our case, but when dealing with more complex components, this can lead to redundant functions declarations.

In fact, our component could be further simplified if we had a useBoolean hook, that we would use like this:



const Spoil = ({ content }) => {
  const [showSpoil, setShowSpoil] = useBoolean(false);

  return (
    <div className="spoil">
      <button onClick={setShowSpoil.toggle}>
        {showSpoil ? "Hide" : "Show"}
      </button>
      {showSpoil && <div className="spoil-content">{content}</div>}
    </div>
  );
};


Enter fullscreen mode Exit fullscreen mode

See? We didn't add any extra function, and the onClick listener is easier to read. Now, let's move into the implement of this simple hook, shall we? 😎

Implementation

First, we define a function in which we can use the useState hook.



const useBoolean = (initialValue) => {
    const [value, setValue] = useState(initialValue)

    return [value, setValue]
}


Enter fullscreen mode Exit fullscreen mode

⚠️ Be careful: you'll only be able to use the useBoolean function (or should I say hook) in React components, as it uses the useState hook.

So far, we've just created an alias for the useState hook.

Not very useful...😅

The interesting part comes now: instead of having the setValue function in the return array, we will use an object that will contain 3 methods:

  • toggle() to toggle the value
  • on() to set the value to true
  • off() to set the value to false

Our hook now looks like this:



const useBoolean = (initialValue) => {
    const [value, setValue] = useState(initialValue)

    const updateValue = useRef({
        toggle: () => setValue(oldValue => !oldValue),
        on: () => setValue(true),
        off: () => setValue(false)
    })

    return [value, updateValue.current]
}


Enter fullscreen mode Exit fullscreen mode

And here it is, you've just created your first custom hook, congratulations! 🥳

Usage



const Articles = () => {
  const [articles, setArticles] = useState([])
    const [isLoading, setIsLoading] = useBoolean(false)
    const [isError, setIsError] = useBoolean(false)

    useEffect(() => {
        setIsLoading.on()
        fetch(...)
            .then(res => res.json())
            .then(setArticles)
            .catch(setIsError.on)
            .finally(setIsLoading.off)
  }, [])

    return ...
}


Enter fullscreen mode Exit fullscreen mode

⚠️ Be careful: you can't use setIsLoading(true) as we don't export a function anymore but an object.

See how the above snippet is very easy to read? 😎

Conclusion

Thanks for reading me. I hope you enjoyed this article, and if that's the case, don't hesitate to have a look at my other ones. Also, feel free to post some comments if you have any questions, or if you just want to say "hi". 👋


Support Me

If you wish to support me, you can buy me a coffee with the following link (I will then probably turn that coffee into a new custom hook... ☕)

Buy Me A Coffee


References

https://reactjs.org/docs/hooks-overview.html

https://en.wikipedia.org/wiki/Single-responsibility_principle

Top comments (15)

Collapse
 
drfrost profile image
Mike Eling • Edited

I suggest you memoize the updateValue API because now it's being recreated on every rerender (state change).

You can use a ref for that, let's say:

const [value, setValue] = useState(initialValue ?? false);
const updateApiRef = useRef(null); // null just for initialization.

if(updateApiRef.current === null)
{
   // This will initialize the API with a memoized object.
    updateApiRef.current = {
        toggle: () => setValue(oldValue => !oldValue),
        on: () => setValue(true),
        off: () => setValue(false),
    }
}
// At this point, updateApiRef.current is not equal to null anymore, before even returning it.
return [value, updateApiRef.current];
Enter fullscreen mode Exit fullscreen mode

I hope this makes sense, writing code on a phone is rly hard 🥵

Collapse
 
meikatz profile image
Gregor Mitzka • Edited

Or you just could use useMemo() for this, much easier:

const updateValue = useMemo(() => ({
  toggle: () => setValue(oldValue => !oldValue),
  on: () => setValue(true),
  off: () => setValue(false),
}), [ setValue ]);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
link2twenty profile image
Andrew Bone • Edited

You don't need setValue as a dep (because it's part of useState). I'd probably say useRef is better but you'd do it similarly to how you have.

const [value, setValue] = useState(initialValue)
const updateValue = useRef({
  toggle: () => setValue(oldValue => !oldValue),
  on: () => setValue(true),
  off: () => setValue(false),
});

return [value, updateValue.current]
Enter fullscreen mode Exit fullscreen mode

That being said I generally just use useState without any Boolean hook.

Thread Thread
 
iamludal profile image
Ludal 🚀

You're right, I think useRef makes sense in this case and the way you used it. Thanks for pointing that out.

Collapse
 
fidaay profile image
fidaay

Why not just

setShowSpoil(!showSpoil);
Enter fullscreen mode Exit fullscreen mode

?

Collapse
 
iamludal profile image
Ludal 🚀 • Edited

It is a good practice to use the "function" form since the new value depends on the previous statue value. In most cases, both solutions will work correctly, but it is possible to have some kind of desynchronization (and so unexpected behaviors) when using your version.

A few keys here (though it is not exactly the same question as you) : stackoverflow.com/questions/610542...

Collapse
 
fidaay profile image
fidaay

That "desynchronization" occurs because of states mutations are asynchronous, and it shouldn't be treated as a problem nor unexpected behavior.

Collapse
 
kraskaska profile image
Kraskaska

well yes but actually no

Collapse
 
freetimeplay profile image
freetimeplay • Edited


const updateValue = useRef({
toggle: () => setValue(oldValue => !oldValue),
on: () => setValue(true),
off: () => setValue(false)
})

Why do we need useRef here instead of just return each function separately ?

Collapse
 
iamludal profile image
Ludal 🚀

The goal of useRef is to make sure that these functions are not recreated every time the useBoolean hook is called. Internally, it is nothing more that a function call, and this function could actually be called many times if your component renders a lot.

By using useRef, we reuse the functions we created earlier without recreating them, which should improve the performance (though I didn't run any benchmarks by myself, so the performance gain might be big or small, but it is still a good practice).

Depending on the size of your application, removing useRef and returning the functions directly might not have any impact at all (or might even be faster, who knows).

Hope this helps, don't hesitate if something was not clear. :)

Collapse
 
joeattardi profile image
Joe Attardi

I don't know if we need an abstraction over a boolean value?

Collapse
 
iamludal profile image
Ludal 🚀

Thanks for your comment! Indeed, we probably don't need so much abstraction (though it can simplify the overall code in a way). This one was more for educational purposes to understand the logic behind creating custom hooks, in order to implement more complex ones. 😁

Collapse
 
oliverflint profile image
Oliver Flint • Edited

HI @ludal , Are you aware that fluentui already has this exact hook... npmjs.com/package/@fluentui/react-...

Collapse
 
iamludal profile image
Ludal 🚀

Hi Oliver, I didn't know about Fluent UI, but I know that Chakra UI also has such a hook (more info here), as well as other packages I guess. 🙂

Some comments may only be visible to logged-in visitors. Sign in to view all comments.