useData - useState improved!

irfanismail,reactstate management

First off, React useState hook sucks.

It can only manage a single primitive or an object one at a time. It's ok if theres 1-2 in a component, but the moment you let it loose, 100% bet your codebase looks like this:

const [open, setOpen] = useState(false)
const [loading, setLoading] = useState(true)
const [name, setName] = useState('name')
const [age, setAge] = useState(20)
const [car, setCar] = useState('whatever')
 
// ...

It's ridiculous to look at, and will be a hard reject if I see it in my PR reviews.

So, what does React team recommend as an alternative? useReducer (opens in a new tab)

import { useReducer } from 'react'
 
function reducer(state, action) {
  if (action.type === 'incremented_age') {
    return {
      age: state.age + 1
    }
  }
  throw Error('Unknown action.')
}
 
export default function Counter() {
  const [state, dispatch] = useReducer(reducer, { age: 42 })
 
  const onClick = () => {
    dispatch({ type: 'incremented_age' })
  }
 
  return (
    <>
      <button onClick={onClick}>Increment age</button>
      <p>Hello! You are {state.age}.</p>
    </>
  )
}

Astaga, another ridiculous hook to look at. My biggest problem with this: it is a complicated pattern for no good reason, which makes it stupid af. Why on earth would you want all your functions to be packed in a single reducer, discernible only by type argument. Unbelievable. A state manager should only do 1 thing and 1 thing only, just manage the state! Leave the function writing outside. Goodness...

So, what now? Zustand? Redux? Jotai? any random 3rd party library...

I have nothing against these libraries, but I just want a simple hook to manage multiple state in a component. It doesn't have to be complicated.

So, just write your own custom state hook. Here's what works for us.

import { useReducer, useRef } from 'react'
 
type MutatorType = 'SET' | 'RESET'
 
interface SingleMutator<K extends keyof T, T> {
  key: K
  value: T[K]
}
 
// Enhance reducer to be generic and enforce action payload types based on the state type
function reducer<T>(
  state: T,
  action: { type: MutatorType; payload: SingleMutator<keyof T, T> | T }
) {
  switch (action.type) {
    case 'SET':
      const { key, value } = action.payload as SingleMutator<keyof T, T>
      return { ...state, [key]: value }
    case 'RESET':
      return { ...state, ...(action.payload as T) }
    default:
      throw new Error('Unrecognized dispatch command')
  }
}
 
/**
 * Universal state hook. General purpose state management
 * Accept a generic type, T which represents the type of the state
 * @param data State
 * @returns data, setData
 */
export const useData = <T extends Record<string, any>>(data: T) => {
  const original = useRef<T>(data)
 
  const [state, dispatch] = useReducer(reducer, data)
 
  const setData = <K extends keyof T>(key: K, value: T[K]) => {
    dispatch({ type: 'SET', payload: { key, value } })
  }
 
  const reset = (override?: T) => {
    dispatch({ type: 'RESET', payload: override ?? original.current })
  }
 
  return {
    data: state as T,
    setData,
    reset
  }
}

The code should be self-explanatory and here is how you would use it:

const { data, setData } = useData({
  name: 'John',
  age: 36,
  profession: 'Engineer'
})
 
setData('name', 'Abu') // name: Abu
setData('age', 54) // age: 54
 
console.log(data) // { name: "Abu", age: 54, profession: "Engineer" }

We can probably do without the reducer function and all, oh well. But, see how easy that is, works like a map, and the API remains simple enough that anyone can guess what it does.

Also, big shoutout to @amalasyrafs (opens in a new tab) for providing awesome type-safety & autocompletion to the hook! Now, TS can scream at you if you make a mistake. Check this out, ngl this is every TS dev's wet dream:

usedata_1 usedata_2 usedata_3

Hope you learn something from here, ciao!

© Irfan Ismail.RSS