Today I learned

State management with React hooks

You don't always need Redux. Here's how you may use custom hooks instead

React now comes with hooks that can be used to manage state and propagate it throughout your app. In essenceyou can do Redux without using Redux! I've been trying to use useState and useContext to manage a large block of state in a React app. In think it's a great alternative to Redux.


Custom state manager

Defining the hook

Let's write our state manager as a custom React hook. This hook is a thin wrapper around React's useState hook. Instead of giving a setState() function, it gives a bunch of setState macros (ie, actions).

useAppState.js
import { useState } from 'react'

/**
 * Our custom React hook to manage state
 */

const useAppState = () => {  const initialState = { count: 0 }

  // Manage the state using React.useState()
  const [state, setState] = useState(initialState)

  // Build our actions. We'll use useMemo() as an optimization,
  // so this will only ever be called once.
  const actions = useMemo(() => getActions(setState), [setState])

  return { state, actions }
}

// Define your actions as functions that call setState().
// It's a bit like Redux's dispatch(), but as individual
// functions.
const getActions = (setState) => ({
  increment: () => {
    setState((state) => ({ ...state, count: count + 1 }))
  },
  decrement: () => {
    setState((state) => ({ ...state, count: count - 1 }))
  },
})

export default useAppState

Using the hook

You can use the useAppState() hook in your React components. It will provide the current state and the actions.

MyApp.js
import { useAppState } from './useAppState'

/**
 * Our top-level app component
 */

const MyApp = () => {
  // Use the custom hook we wrote earlier
  const { state, actions } = useAppState()
  return (
    <div>
      <span>{state.count}</span>

      <button onClick={actions.increment}> + </button>
      <button onClick={actions.decrement}> - </button>
    </div>
  )
}

Passing actions

Passing as props

The functions in actions are plain old functions that you can pass down into child components. You can pass them to event props as-is.

<Toolbar
  count={state.count}
  onIncrement={actions.increment}  onDecrement={actions.decrement}/>

Passing many actions down

If you need to pass many functions down, it's also possible to simply pass the entire actions object downwards.

<Toolbar
  count={state.count}
  actions={actions}/>

Working with contexts

The one essential feature of react-redux is the way the application's state is available to all its descendant components. This can be done with React contexts.

Exporting a context

In our useAppState.js file, we'll export a context created with React.createContext, along with a new custom hook for using the app context.

useAppState.js
import { useState, useContext } from 'react'

const AppContext = React.createContext({})

// This is for the top-level component, providing `state`
// and `actions`. (Same function as in the examples above.)
const useAppState = () => {  // ...
}

// Sub-components can use this function. It will pick up the
// `state` and `actions` given by useAppState() higher in the
// component tree.
const useAppContext = () => {  return useContext(AppContext)
}

export { AppContext, useAppState, useAppContext }

Defining a provider

In your app's root, use the AppContext.Provider component. This makes it possible to use the useAppContext() hook in child components.

MyApp.js
import { AppContext, useAppState } from './useAppState'
import Toolbar from './Toolbar'

// Top-level app component
const MyApp = () => {
  const { state, actions } = useAppState()

  return (
    <AppContext.Provider value={{ state, actions }}>      <div>
        {/* Components here will consume the value */}
        {/* given to the provider above. */}
        <Toolbar />
      </div>
    </AppContext.Provider>
  )
}

Consuming it

The useAppContext hook allows you to use the app state anywhere inside the Provider tree. This is similar to using react-redux's connect() function.

Toolbar.js
import { useAppContext } from './useAppState'

const Toolbar = () => {
  // In other components such as this, we can use the
  // useAppContext() hook to fetch the 'state' and 'actions'
  // from higher up in the component tree.
  //
  // This takes the `value` given in `<AppContext.Provider>`
  // in the top-level component above (MyApp.js).
  const { state, actions } = useAppContext()
  return (
    <div>
      <button onClick={actions.increment}> + </button>
      <button onClick={actions.decrement}> - </button>
    </div>
  )
}

Epilogue

Thanks for reading this! I've done a few edits to this article since it was first published:

  • Changed setState({ ...state }) to setState(state => ({ ...state })), because the latter would cause trouble when many setState's are called.

  • Added useMemo() the actions block to optimize it and make it faster.

You have just read State management with React hooks, written on April 10, 2019. This is Today I Learned, a collection of random tidbits I've learned through my day-to-day web development work. I'm Rico Sta. Cruz, @rstacruz on GitHub (and Twitter!).

← More articles