Introduction to Custom React Hooks
Seb Toombs
Mar 26 2020 ()
5 min read
How to write custom React hooks
If you've been around the the React world lately, you'll no doubt have noticed or used hooks. Hooks were introduced into React in 16.8 and were kinda a big deal.
React hooks gave us the ability to have state in function components, reduce the need for render props, and just generally make DX and life better.
As always, the React docs are a good place to start on this, so I recommend checking them out too.
Custom hooks in React are a way to reuse functionality (particularly functionality involving stateful logic) between components. You could just use a function to share between components, but then you don't get all the goodness of being able to access component lifecycle events and state. React hooks let you 'hook' (see, it's in the name) into things like component lifecycle (mount, unmount, etc), state, and other hooks.
So what is a custom hook, and how is it different to just a function? Well, a custom hook is just a function that uses other hooks. These might be other custom hooks, or React's basic hooks (useState, useEffect, etc). If you don't use any hooks, you've just got a function, not a hook.
The convention for naming hooks is to prefix your function with "use" (as in "useState", "useEffect" etc). For example if I were to create a custom hook to use the scroll distance down the page, I might name it "useScrollDistance". This is by no means a rule, and your hook will still work if you name it "myCoolHook", but it's a useful convention allows others to easily recognise your function as a hook.
Custom hook example
To help explain how custom hooks work, I'm going to show you a quick example of hook that you might even use in a real app (in fact it's in the codebase of one of mine). We'll make a hook that let's us keep track of the scroll distance down a page.
Quick react hook recap
First, we'll just take a quick recap of how react hooks work; we'll use useState and useEffect as examples.
If we have a simple component, which needs some state, we can use useState to keep track of it like this;
import React, {useState} from 'react'
const myComponent = () => {
//This is our hook call to useState
// useState will return an array [stateValue, stateSetter]
// useState can also take the initial state as an argument (0)
const [counterValue, setCounterValue] = useState(0);
const incrementCounter = () => {
setCounterValue(counterValue+1);
}
return (
<div>
<p>Counter Value: {counterValue}</p>
<div>
<button onClick={incrementCounter}>Increment Counter</button>
</div>
</div>
);
}
Here we have a simple component which calls useState, with the argument 0. This returns an array containing the state variable, and a function to update that state variable. The argument 0 is the initial value we would like to store in the state variable.
Ok, lets say we'd like to know when the counterValue has changed, and trigger some action, a side-effect. We can do that with the useEffect hook. useEffect will subscribe to changes in the variable you specify.
import React, {useState, useEffect} from 'react'
const myComponent = () => {
//This is our hook call to useState
// useState will return an array [stateValue, stateSetter]
// useState can also take the initial state as an argument (0)
const [counterValue, setCounterValue] = useState(0);
const incrementCounter = () => {
setCounterValue(counterValue+1);
}
// Using the useEffect hook
// The first argument is the callback to trigger
// when the value changes
// The second argument is the variable we'd like to track
useEffect(() => {
console.log(`The new counter value is: ${counterValue}`)
}, [counterValue]);
return (
<div>
<p>Counter Value: {counterValue}</p>
<div>
<button onClick={incrementCounter}>Increment Counter</button>
</div>
</div>
);
}
Now we've modified the component to log the value of the counter every time it changes. This isn't a particularly useful real-world scenario, but hopefully it demonstrates how the hook works.
We added a call to useEffect and passed it a callback to run every time the variable we're interested in changes. In the second argument, we passed in an array of the variables we'd like to keep track of.
A really simple custom hook
Using the example above, let's say we wanted to reuse this functionality (logging a state variable) in multiple places. I can't see why, but bear with me.
What we can do is compose a custom hook from our basic useState and useEffect hooks and make it reusable.
import React, {useState,useEffect} from 'react';
//This is our custom hook
//it abstracts the logic & state of our counter
const useCounterValue = (initialValue=0) => {
// Set up the state, same as before
const [counterValue, setCounterValue] = useState(initialValue);
// Create our count increment function
const incrementCounter = () => {
setCounterValue(counterValue+1);
}
// Watch for changes in the value
useEffect(() => {
console.log(`The new counter value is ${counterValue}`);
}, [counterValue])
// Return the state variable and our
// increment function
return [counterValue, incrementCounter];
}
const myComponent = () => {
// Use our custom hook
const [counterValue, incrementCounter] = useCounterValue(0);
return (
<div>
<p>Counter Value: {counterValue}</p>
<div>
<button onClick={incrementCounter}>Increment Counter</button>
</div>
</div>
);
}
Here we created a custom hook useCounterValue which does exactly the same thing as our previous component, but now it's reusable. We could add this code into multiple components and consume it anywhere .
(BIG note: the state won't be shared simultaneously. The state is specific to the component you're using the hook in!)
I know this example is a bit contrived, but hopefully it demonstrates some of the power and simplicity of custom hooks!
A real example
Ok, now we're going to make a real custom React hook, one that would actually be useful!
We'll make a custom hook called useScrollDistance which will tell us how far down the page the user has scrolled. Examples of use cases for this might include; "polyfilling" position: sticky, infinite loaders, animation triggers etc. You could even adapt this to know how far a different scrollable element has been scrolled (think; fullpage style transitions etc).
I think I've wasted enough of your time already, so let's just do it. Here's our custom hook;
// useScrollDistance.js
import {useState, useEffect} from 'react';
const useScrollDistance = () => {
//Set up a state variable and
// store the initial value of window.scrollY
const [scrollDistance, setScrollDistance] = useState(window.scrollY);
//Set up a handler to update our state variable
//on scroll events
const onScrollHandler = () => {
setScrollDistance(window.scrollY);
}
//call useEffect to listen to component mount & unmount
useEffect(() => {
//Add a javascript event listener to the window
//to listen to scroll events
window.addEventListener('scroll', onScrollHandler);
//Return a function to run on unmount
return () => {
//Don't forget to remove any event listeners on unmount!
window.removeEventListener('scroll', onScrollHandler);
}
}, []);
//Finally return the value of interest
return scrollDistance;
}
export default useScrollDistance;
That's our hook, it uses useEffect to run a function on component mount which binds an event listener to the window's scroll event. Our event handler then updates our state with the new value of the scroll distance.
And here's how we might use it;
//Import our custom hook
import useScrollDistance from './useScrollDistance'
const myComponent = () => {
//Call our hook
const scrollDistance = useScrollDistance();
//Render the value
return (
<p>Scroll distance: {scrollDistance}</p>
);
}
And this is what that might look like (with a little styling applied)
You probably wouldn't want to just slap this code into production as is. For one, you might want to add some throttling to the event listener (see my post about understanding throttling, or my post about using custom hooks to listen to DOM events for an example).
Your turn
Hopefully this has shown you how easy it is to create custom hooks, and how useful and powerful they can be. Check out my other posts on the topic for a bit more info if you're interested.
Anything a bit vague? Need more info? Let me know! Hit me up on twitter @baffledbasti.