article

Custom React Hooks

or: How I Learned to Stop Worrying and Love useReducer

Richard Turner
Richard Turner
5 mins read
Fishing hooks

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
App screenshot
JDLT use cookies. See our Privacy Policy for details.