Custom React Hooks allow us to extract component logic into reusable functions. Custom Hooks look very much like normal helper functions, except they can maintain component state and perform effects. There are many common actions that we do in our React applications that can be wrapped up in a custom Hook.
So let's take a look at the implementations of 8 different custom Hooks. Each implementation will include code comments as well as a potential use case. And since I'm a TypeScript fan, I'll also provide a second implementation in TypeScript when there is any TypeScript uniqueness. 😄
You'll find flavors of many of these custom Hooks in the react-use
package. So if you find yourself using a lot of these Hooks, you might as well import the package.
Let's jump in!
useInitialMount
Sometimes we only want to run an effect the very first time the component mounts. In my experience, the majority of these times have been firing off a tracking event. Usually I'll maintain a hasSent
local variable that I flip from false
to true
after I've sent it the first time. But instead, we can use a useInitialMount
custom Hook.
import { useRef, useEffect } from 'react'
const useInitialMount = () => {
// refs exist across component re-renders, so
// we can use it to store a value for the
// subsequent renders. We're tracking if it's
// the first render, which is initially `true`
const isFirst = useRef(true)
// the very first render, the ref will be
// `true`. but we immediately set it to `false`
// so that every render after will be `false`
if (isFirst.current) {
isFirst.current = false
// return true the very first render
return true
}
// return false every following render
return false
}
const Page = ({ pageName, items }) => {
const isInitialMount = useInitialMount()
useEffect(() => {
// only call `trackEvent` for initial mount.
// don't call it ever again, even if
// `pageName` or `items.length` change
if (isInitialMount) {
trackEvent(pageName, items.length)
}
}, [pageName, items.length, isInitialMount])
// render UI
}
usePrevious
Sometimes we need to know the previous value of props or state to compare it to a current value. The usePrevious
Hook is so common that it shows up in the React Hooks FAQ.
const usePrevious = (value) => {
const ref = useRef()
useEffect(() => {
ref.current = value
})
return ref.current
}
However, the FAQ code above has a potential gotcha in that if the component re-renders for another reason (for instance, a change in another state value), then our previous value gets overwritten even though it technically hasn't changed.
We likely want a usePrevious
that only updates the previous value if it differs from the new value.
import { useRef, useState, useEffect } from 'react'
const usePrevious = (value) => {
// use refs to keep track of both the previous &
// current values
const prevRef = useRef()
const curRef = useRef(value)
const isInitialMount = useInitialMount()
// after the initial render, if the value passed in
// differs from the `curRef`, then we know that the
// value we're tracking actually changed. we can
// update the refs. otherwise if the `curRef` &
// value are the same, something else caused the
// re-render and we should *not* update `prevRef`.
if (!isInitialMount && curRef.current !== value) {
prevRef.current = curRef.current
curRef.current = value
}
return prevRef.current
}
const Example = () => {
const [time, setTime] = useState(() => new Date())
const [count, setCount] = useState(0)
// use `usePrevious` to keep track of the `count`
// from the last time it changed
const prevCount = usePrevious(count)
// update `date` every 1 sec to have another state updating
useEffect(() => {
const intervalId = setInterval(() => setTime(new Date()), 1000)
return () => clearInterval(intervalId)
})
return (
<div>
<button onClick={() => setCount((curCount) => curCount + 1)}>+</button>
<button onClick={() => setCount((curCount) => curCount - 1)}>-</button>
<p>
Count: {count}, Old Count: {prevCount}
</p>
<p>The time is {time.toLocaleTimeString()}.</p>
</div>
)
}
Did you catch how we're using the useInitialMount
Hook here in usePrevious
? I love when I can reuse Hooks within other Hooks.
And in TypeScript, the code is more or less the same, except we use generics so that the type of curValue
can be arbitrary, but enforce that the return value has to be the same type.
import { useRef } from 'react'
const usePrevious = <T>(value: T): T | undefined => {
const prevRef = useRef<T>()
const curRef = useRef(value)
const isInitialMount = useInitialMount()
if (!isInitialMount && curRef.current !== value) {
prevRef.current = curRef.current
curRef.current = value
}
return prevRef.current
}
The react-use
package has a similar Hook that's called usePreviousDistinct
to distinguish itself from the basic usePrevious
outlined in the React Hooks FAQ. It also provides a compare function which can be passed as the optional second parameter to control when the new value is different from the previous value. The default compare function is strict equality like we used above. But if the value being stored is an array or object, you'll likely want a deep-equals comparison instead.
useUniqueId
Sometimes in React we need to create a unique ID to use an id
for DOM elements. The ID itself doesn't really matter, but is needed for associating two elements together for accessibility purposes using ARIA attributes within a complex component.
The trick is that we don't want to generate a new ID for every render. Once we've generated an ID for the component, we want it to remain the same. Our good ol' friend useRef
will come to the rescue again, and we can wrap up the logic into something easily reusable.
import { useRef } from 'react'
let GLOBAL_ID = 0
const useUniqueId = () => {
const idRef = useRef('')
const isInitialMount = useInitialMount()
// generate the ID for the first render
// and store in the ref to remain for
// subsequent renders
if (isInitialMount) {
GLOBAL_ID += 1
idRef.current = `id${GLOBAL_ID}`
}
return idRef.current
}
const NavMenu = ({ items }) => {
const id = useUniqueId()
const buttonId = `${id}-button`
const menuId = `${id}-menu`
return (
<>
<button id={buttonId} aria-controls={menuId}>
+
</button>
<ul id={menuId} aria-labelledby={buttonId} role="menu">
{items.map((item) => (
<li key={item.id}>
<a role="menuitem" href={item.url}>
{item.title}
</a>
</li>
))}
</ul>
</>
)
}
Once again we get to use a previously created custom Hook (useInitialMount
) to help build a new Hook.
React actually had a proposal for a built-in Hook to solve this exact problem and ensure the ID generated client-side matched the one generated server-side. It was called useOpaqueIdentifier
. Not sure what exactly happened to it and why it hasn't been released, but I wrote about it last year in post entitled New React useOpaqueIdentifier hook coming soon. Lesson learned. 😂
useIsMounted
In my previous post on Handling async React component effects after unmount, I outline 4 ways to solve the problem where we try to update the state of a component after it has already unmounted. We prevent updating the state with the result of a fetch
call when the response returned after the component had already been unmounted.
All the solutions resolve around keeping track of if the component is still mounted. So we created a custom Hook for it called useIsMounted
.
import { useEffect, useRef, useCallback } from 'react'
// returns a function that when called will
// return `true` if the component is mounted
const useIsMounted = () => {
// the ref to keep track of mounted state across renders
const mountedRef = useRef(false)
// helper function that will return the mounted state.
// using `useCallback` because the function will likely
// be used in the deps array of a `useEffect` call
const isMounted = useCallback(() => mountedRef.current, [])
// effect sets mounted ref to `true` when run
// and the sets to `false` during effect cleanup (i.e. unmount)
useEffect(() => {
mountedRef.current = true
return () => {
mountedRef.current = false
}
}, [])
return isMounted
}
const Results = () => {
const [items, setItems] = useState([])
const isMounted = useIsMounted()
useEffect(() => {
fetchItems().then((newItems) => {
// only set state if the component
// is still mounted after receiving
// the async data
if (isMounted()) {
setItems(newItems)
}
})
}, [isMounted])
// render UI
}
useMedia
Media queries allow us to change our UI based upon a host of media features (most commonly the size of the window). Media queries are normally used in CSS, but the matchMedia()
API allows us to execute media queries in JavaScript when necessary. Maybe we need to change props or render entirely different components depending on the results of a media query.
import { useState, useEffect } from 'react'
const useMedia = (query) => {
// initialize state to current match value
const [matches, setMatches] = useState(() => window.matchMedia(query).matches)
const isMounted = useIsMounted()
useEffect(() => {
if (!isMounted()) {
return
}
const mediaQueryList = window.matchMedia(query)
const listener = () => {
// update `matches` state whenever query match changes.
// `isMounted()` check is for extra protection in case
// listener somehow fires in between unmount and
// listener removal
if (isMounted()) {
setMatches(mediaQueryList.matches)
}
}
mediaQueryList.addListener(listener)
// sync initial matches again
setMatches(mediaQueryList.matches)
return () => {
mediaQueryList.removeListener(listener)
}
}, [query, isMounted])
return matches
}
const Example = () => {
const isSmall = useMedia('(max-width: 480px)')
return <p>Screen is {isSmall ? 'small' : 'large'}</p>
}
useRafState
There are some DOM events, like window resize
or document scroll
, the fire a lot. They fire much faster than the DOM can update, so if we try to update the DOM for each event, our app feels sluggish.
We can try to work around this problem by manually debouncing or throttling the events, but there's an interesting alternative. We can make use of requestAnimationFrame
to debounce in a different way by only setting the state on the next animation frame.
I found this useRafState
Hook on react-use
and had to share it because it'll making this debouncing much easier. Its interface is identical to useState
. It wraps useState
, only setting the state using requestAnimationFrame
.
import { useState, useCallback, useRef, useEffect } from 'react'
const useRafState = (initialState) => {
// this is the actual state
const [state, setState] = useState(initialState)
// keep track of the `requestAnimationFrame` request ID
// across renders and successive calls to `useRafState`
const requestId = useRef(0)
// the actual state setter we'll return.
// using `useCallback` so that it's memoized
// just like `setState`
const setRafState = useCallback((value) => {
// cancel active request before making next one.
// this is debouncing.
cancelAnimationFrame(requestId.current)
// create new request to set state on animation frame
requestId.current = requestAnimationFrame(() => {
setState(value)
})
}, [])
// cancel any active request when component unmounts
useEffect(() => {
return () => cancelAnimationFrame(requestId.current)
})
return [state, setRafState]
}
const Example = () => {
const [width, setWidth] = useRafState(0)
useEffect(() => {
const handleResize = () => {
setWidth(window.innerWidth)
}
window.addEventListener('resize', handleResize)
// set initial value
setWidth(window.innerWidth)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [setWidth])
return <p>Window width: {width}</p>
}
The TypeScript for useRafState
is a bit involved because we need to mimic all that useState
can do. Since the state can be anything, we leverage generics once again.
// just like with `useState`, `initialState` is either a value
// or a function that returns a value
// the type of `initialState` is generic and depends on the value
// being passed in
const useRafState = <S>(initialState: S | (() => S)) => {
const [state, setState] = useState(initialState)
const requestId = useRef(0)
// just like with `useState`, `setRafState` is a memoized function
// that either takes a value or a function that accepts
// the previous value and returns the next value
const setRafState = useCallback((value: S | ((prevState: S) => S)) => {
cancelAnimationFrame(requestId.current)
requestId.current = requestAnimationFrame(() => {
setState(value)
})
}, [])
useEffect(() => {
return () => cancelAnimationFrame(requestId.current)
})
// using const assertion simplifies return value type definition
return [state, setRafState] as const
}
For more on as const
, read Use cases for TypeScript const assertions.
useWindowScroll
We may need to track the scroll position of the window in order to know when to pin or unpin some sticky content on the page. And since the document scroll
event fires often, we'll likely want to debounce updating the state. We can build useWindowScroll
using the useRafState
custom Hook we just implemented.
import { useEffect } from 'react'
const getPos = () => ({
x: window.scrollX,
y: window.scrollY,
})
const useWindowScroll = () => {
const [pos, setPos] = useRafState({ x: 0, y: 0 })
useEffect(() => {
const handleScroll = () => {
// `useRafState` will debounce on animation frame
setPos(getPos())
}
window.addEventListener('scroll', handleScroll)
// set initial value
setPos(getPos())
return () => {
window.removeEventListener('scroll', handleScroll)
}
})
return pos
}
const Example = () => {
const { x, y } = useWindowScroll()
return (
<p>
Position: ({x}, {y})
</p>
)
}
useDeepCompareEffect
Last month I wrote a post entitled Object & array dependencies in the React useEffect Hook, which provided four strategies for optimizing useEffect
with object or array dependencies. The basic problem is that useEffect
uses strict equality when checking if individual dependencies have changed. So objects or arrays in useEffect
dependencies can cause unnecessary effect runs even when the objects have the same contents.
If we had a useEffect
that instead performed a deep comparison of the objects or arrays, we wouldn't have to worry about having having them in our dependencies. So let's build one!
import { useRef, useEffect } from 'react'
import isDeepEqual from 'fast-deep-equal/react'
const useDeepCompareEffect = (effect, deps) => {
// use a ref to keep track of deps across renders
const depsRef = useRef()
// if the new deps don't deep equal the prev ones
// stored in the ref, update the ref. If they *do*
// deep equal, the ref will remain unchanged and
// the array passed to `useEffect` will be the exact
// same array as before so the basic strict equality
// will work
if (!depsRef || !isDeepEqual(depsRef.current, deps)) {
depsRef.current = deps
}
useEffect(effect, depsRef.current)
}
const Team = ({ team }) => {
const [players, setPlayers] = useState([])
useDeepCompareEffect(() => {
if (team.active) {
getPlayers(team).then(setPlayers)
}
}, [team])
return <Players team={team} players={players} />
}
Because the useDeepCompareEffect
Hook is a wrapper around the basic useEffect
Hook, it acts like a drop-in replacement for useEffect
. Our code looks almost identical. However, we probably wouldn't want to use useDeepCompareEffect
all the time because it's definitely slower than normal useEffect
because of the deep comparison checks. But in spot cases, it'll definitely come in handy.
Also, if you use eslint-plugin-react-hooks
for linting your React Hooks code (which I hope you do), you'll have to update the react-hooks/exhaustive-deps
rule to include useDeepCompareEffect
in the additionalHooks
configuration. This way you'll still be warned if you forget to list a dependency.
The implementation in TypeScript mirrors the type API of useEffect
:
import { useRef, useEffect, DependencyList, EffectCallback } from 'react'
import isDeepEqual from 'fast-deep-equal/react'
const useDeepCompareEffect = <TDeps extends DependencyList>(
effect: EffectCallback,
deps: TDeps,
) => {
const depsRef = useRef<TDeps | undefined>()
if (!depsRef || !isDeepEqual(depsRef.current, deps)) {
depsRef.current = deps
}
useEffect(effect, depsRef.current)
}
The react-use
package has a similar useDeepCompareEffect
Hook. But its actually built on top of a useCustomCompareEffect
Hook that accepts any compare function that is then used for comparing the dependencies. As a result, it enables supporting useShallowCompareEffect
as well.
So that's it! 😅 There are literally an infinite number of custom Hooks that we can create. But these are 8 custom Hooks that I found especially helpful or particularly interesting.
What custom Hooks do you find yourself using all the time? I'd love to know! Feel free to reach out to me on Twitter at @benmvp to let me know!
Keep learning my friends. 🤓