uidotdev / usehooks

A collection of modern, server-safe React hooks – from the ui.dev team
https://usehooks.com
MIT License
9.29k stars 496 forks source link

useMergeState #8

Closed qodesmith closed 1 year ago

qodesmith commented 5 years ago

I wanted to make a PR but I don't know how Gatsby works so I figured I can leave the code here and let you decide if you'd like to publish it or not.

The idea is to mimc the functionality of the old setState as closely as possible:

  1. Provide an object to be merged into state OR
  2. Provide a function which gets passed the old state and returns a new object to be merged into state.

To me this is especially helpful in 3 scenarios:

  1. You have a lot of state and don't want to have a ton of useState declarations at the top of your component.
  2. You want to stick with what you're used to from setState.
  3. You want to avoid the overhead of using useReducer.

useMergeState

import { useState } from 'react'

// Object-checking helper fxn since this is only meant to work with objects.
const objCheck = thing => {
  if (({}).toString.call(thing) !== '[object Object]') {
    throw '`useMergeState` only accepts objects.'
  }
}

const useMergeState = (initialState = {}) => {
  objCheck(initialState)
  const [state, setState] = useState(initialState)

  /*
    Just like the old `setState` with React classes, you can pass a fxn to `mergeState`.
    Why would you want to do this? Because certain scenarios, such as using the `useEffect` hook
    create closures around values that may become stale or outdated by the time they get used.
    Providing a fxn ensures that fxn has the latest available state object to work with.
  */
  const mergeState = objOrFxn => {
    // Passing a fxn to `mergeState`.
    if (objOrFxn instanceof Function) {
      setState(prevState => {
        const newState = objOrFxn(prevState)
        objCheck(newState)
        return { ...prevState, ...newState }
      })

    // Passing an object to `mergeState.
    } else {
      objCheck(objOrFxn)
      setState(prevState => ({ ...prevState, ...objOrFxn }))
    }
  }

  return [state, mergeState]
}

Example usage

import React from 'react'

const MyComponent = () => {
  const [state, mergeState] = useMergeState({
    date: new Date(),
    num: 1
  })

  // Using an object.
  const button1Click = () => mergeState({ date: new Date() })

  // Using a function.
  const button2Click = () => mergeState(oldState => ({ num: oldState.num + 1 }))

  return (
    <>
      <button onClick={button1Click}>{state.date.toString()}</button>
      <button onClick={button2Click}>{state.num}</button>
    </>
  )
}
TrySound commented 5 years ago
const reducer = (prev, updater) => {
  if (typeof updater === 'function') {
    return { ...prev, updater(prev) }
  } else {
    return { ...prev, updater }
  }
}
const [state, setState] = React.useReducer(reducer, initialState)
qodesmith commented 5 years ago

Well ain't that sexy

qodesmith commented 5 years ago

This refactor for the win:

import { useReducer } from 'react'

const reducer = (prevState, updater) => (
  typeof updater === 'function'
    ? { ...prevState, ...updater(prevState) }
    : { ...prevState, ...updater }
)

const useMergeState = (initialState = {}) => useReducer(reducer, initialState)

export default useMergeState
gragland commented 4 years ago

Sorry for getting to this so late. This is super useful and especially like how it's implemented with useReducer. Might be fun to talk a bit about how React actually builds useState with useReducer. I'm going to do a post on this soon :)