Custom React Hooks
or: How I Learned to Stop Worrying and Love useReducer
It's been some time since our post Why we've moved to React Hooks, feel free to have a quick recap if you need one. In this post we're going to pick up where Rich left off and jump right in to the world of custom React Hooks.
The brave new world of React Hooks has enabled some pretty game-changing shifts in React state management. One of the key benefits to using React Hooks over traditional React Classes is just the pure simplicity of using solely functional components (say goodbye to this
!).
Having said that, while React Hooks flaunts its simplicity with abandon, it has ushered in a whole host of new terminologies and concepts. One of the most elusive and exciting being the useReducer
hook (which should be familiar to those Redux users out there), which essentially allows you to create elaborate custom state management functions.
useState
But before we talk about useReducer
, let’s talk about the most simple hook to grasp: useState
. Probably the most widely used hook, useState
is used to provide a state value and also a setState
function (which is used to change state).
const [state, setState] = useState('hello world')
console.log(state) // 'hello world'
Anything being parsed as the argument when setState
is called becomes the new state value.
setState('goodbye world')
console.log(state) // 'goodbye world'
setState
even takes an anonymous function, which uses the previous state as its first parameter. However, let’s remember that it doesn’t work as if by magic; there’s actually a fairly straightforward useReducer
at work behind the scenes.
useReducer
Below is how you’re able to replicate useState
as a useReducer
hook (see Kent C. Dodds’ brilliant full breakdown of this function).
const useStateReducer = (prevState, newState) =>
typeof newState === 'function' ? newState(prevState) : newState
function useState(initialState) {
return React.useReducer(useStateReducer, initialState)
}
If you don’t speak useReducer
yet, essentially a useReducer
takes in a reducer as its first argument. This is a function which declares how state management is handled (otherwise known as a dispatch function) - in this case, this reducer checks to see whether the newState
value is a function, and returns either the result of that function (with the previous state as an argument) or if it is not a function it just returns ‘newState’. This dispatch function essentially becomes useState
’s setState
function. Now when we declare our state and setState
function using our makeshift useState
it should have the exact same functionality as it did with the pre-made useState
.
Now that you can see how powerful useReducer
can really be, it makes you wonder what else you can do with it. We at JDLT are always looking to improve upon what has become before, so we decided to look at useState
and see if there were any other additional functionality we could add to it by creating our own custom hook. After using useState
a fair bit, it becomes quite obvious that it’s great for managing single item states (a string or an array etc.) but a bit cumbersome when it came to object states. When updating your object state it would become quite messy to continuously spread whole state objects when you only wanted to update one value within that state object, especially when you have large object states.
useObjectState
Hey, presto: we created useObjectState
. useObjectState
is a custom hook which works in the exact same way as useState
, but instead of a setState
function, we get given an updateState
function when it is called, which only requires the updated values to be parsed rather than the whole state object. With some useReducer
magic, we created a reducer that recursively checks each parsed value to see if it is an object, then if it is an object it will go to the next nested level and check if that value is an object etc., otherwise it will simply update that value. For any values that are not parsed into the dispatch function it leaves those values untouched in the state object, eliminating the need to spread whole state objects like before.
import { useReducer } from 'react'
const recursivelyUpdateFields = (parent: any, [key, value]: any): any => {
const field = parent ? parent[key] : null
const isValueObject = value
? typeof value === 'object' && value.constructor === Object
: false
return {
...parent,
[key]: isValueObject
? {
...field,
...Object.entries(value).reduce(recursivelyUpdateFields, field),
}
: value,
}
}
const applyUpdate = (update: any, prevState: any): any =>
Object.entries(update).reduce(recursivelyUpdateFields, prevState)
const updateObjectStateReducer = (prevState: any, update: any): any => {
return typeof update === 'function'
? update(prevState)
: applyUpdate(update, prevState)
}
const useObjectStateCustomHook = (initialState: any): any => {
return useReducer(updateObjectStateReducer, initialState)
}
Things get really exciting when you use this reducer in conjunction with the useContext
hook (if you’re unfamiliar with the React Context API, I recommend reading The React useContext Hook in a Nutshell by Stephen Hartfield).
import React, { createContext, useContext, useReducer } from 'react'
const StateContext = createContext({})
const ObjectStateProvider = ({ initialState, children }: any): any => (
<StateContext.Provider
value={useReducer(updateObjectStateReducer, initialState)}
>
{children}
</StateContext.Provider>
)
const useServiceState = (): any => useContext(StateContext)
Using the ObjectStateProvider
we can create a global state object that is accessible in all of our child components (via the useServiceState
function) which has an inherited dispatch function that updates object state values recursively. This really tidies up our code from lots of setState
functions and passing of props to various components. Whilst it won’t solve all your problems (you may want to separate out your various states into their own contexts to save on rerenders), this is a really nice way of lifting the barrier for entry into the world of useReducers
. And before you know it, you’ll be creating custom hooks for everything!
Ready to dive in?
At JDLT, we manage IT systems & build custom software for organisations of all sizes.
Get in touch to see how we can help your organisation.
Book a call