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 essence—you 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.
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
).
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
You can use the useAppState()
hook in your React components. It will provide the current state
and the actions
.
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>
)
}
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}/>
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}/>
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.
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.
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 }
In your app's root, use the AppContext.Provider component. This makes it possible to use the useAppContext()
hook in child components.
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>
)
}
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.
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>
)
}
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.