DEV Community

Cover image for useReducer: Beyond useState for Complex State Logic
Kada Guetouache
Kada Guetouache

Posted on

useReducer: Beyond useState for Complex State Logic

React's useState hooks is very powerful tool to manage states withing functional components. It's simple and perfect for handling simple state changes like updating a boolean, a string or a counter. but as the state grows in complexity like the state of form fields useState hooks show it's limitations.

Dealing with multiple interrelated pieces of state or complex state logic can be very difficult to manage using useState. This is where useReducer hook comes in, It provider a structured and maintainable approach for dealing with complex state scenarios.

The benefits of useReducer hook.

useReducer hook centralize state logic and creates a single source of truth for updating state by encapsulating state logic within reducer function. The reducer is a pure function which ensure that given same input always produce same output, which makes the state more predictable and testable.

This hook also improve and organize code by separating state logic from component which also result in performance enhancement sometimes.

Understanding useReducer.

there is 3 core concepts that must be understand in useReducer hook which are:

  • State: similar to useState it represent the data that the component need to render.
  • Action: an Object that has type property to identify the action and custom payload for additional data.
  • Reducer: it's a pure function that accepts the state and action as parameters and return the state it is within the reducer function how the state change is determined.

In this example we will create a simple counter by pressing a button using useReducer, Then later add a name and updating to show an example of complex and unrelated state updates.

Implementing useReducer logic.

First create a constant with initial state above the component.

const initialState = {counter: 0}

const App = () => {
  ...
}
export default App;
Enter fullscreen mode Exit fullscreen mode

In the component the reducer function must be created outside the functional component. That function will accept a state and action as parameters, then will use switch statement to determine the type of the action and change the state if the action is unknown then will return just the state and the component won't re-render because the state hasn't changed as it's determined by Object.is.

const initialState = {counter: 0}

const reducer = (state, action) => {
  switch(action.type) {
    case "increment_counter":
      return {
         counter = state.counter + 1
      }
    default: 
      return state
  }
}

const App = () => {
  ...
}
export default App;

Enter fullscreen mode Exit fullscreen mode

Inside the component we call useReducer, which accepts the reducer function and an initial state.
PS: the initial state can be calculated and passed as function but we must pass reducer the props of state function then the state function like this

const initialStateFn = (name) => {...}
const [state dispatch] = useReducer(reducer, name, initialStateFn)
Enter fullscreen mode Exit fullscreen mode

we can access the state and dispatch function from useReducer hook. the dispatch function accepts an object with the type of the action and options parameter to determine how the state will be updated in reducer function.

import {useReducer} from 'react'

const initialState = {counter: 0}

const reducer = (state, action) => {
  switch(action.type) {
    case "increment_counter":
      return {
         counter = state.counter + 1
      }
    default: 
      return state
  }
}

const App = () => {
  const [state, dispatch] = useReducer(reducer, initialState)
  return (...)
}
export default App;
Enter fullscreen mode Exit fullscreen mode

now we can display and update the state inside the return statement of component like so

const App = () => {
  ...
  return (
   <div>
     <button onClick={() => dispatch({type: 'increment_count'})>Increment counter</button>
     <p>{state.counter}</p>
   </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Making state more complex

Now we also want to add input and type something on it, Will have to update the logic in 3 different place.
Update the initial state to include like a name

const initialState = {
  counter: 0,
  name: ''
}
Enter fullscreen mode Exit fullscreen mode

Update switch statement to handle the action of updating the input will call it updateName. we also need return new state object to keep the previous state intact in this example the counter will kept it's previous state when the state of the name is been updated.
also notice we are using action.nextName which is an optional parameter passed to dispatch function dispatch({type: 'changeName', nextName: nextName}), well add next.

const reducer = (state, action) => {
  switch(action.type) {
    case "increment_counter":
      return { 
         ...state, 
         counter = state.counter + 1
      }
    case "updateName":
      return {
         ...state,
         name = action.nextName
      }
    default: 
      return state
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Update the markup in component.

const App = () => { 
  ...
  return(
    <div>
      ...
      <input value={state.value} 
        onChange={(event) => dispatch({
          type: 'updateName', 
          nextName: event.target.value
        })}
      />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

In the end will have something like this.

import {useReducer} from 'react'

const initialState = {
  counter: 0,
  name: ''
}

const reducer = (state, action) => {
  switch(action.type) {
    case "increment_counter":
      return { 
         ...state, 
         counter = state.counter + 1
      }
    case "updateName":
      return {
         ...state,
         name = action.nextName
      }
    default: 
      return state
  }
}

const App = () => {
  const [state, dispatch] = useReducer(reducer, initialState)
  return (
   <div>
     <button onClick={() => dispatch({type: 'increment_count'})>Increment counter</button>
     <p>{state.counter}</p>
     <input value={state.value} 
        onChange={(event) => dispatch({
          type: 'updateName', 
          nextName: event.target.value
        })} />
   </div>
  )
}
export default App;
Enter fullscreen mode Exit fullscreen mode

Conclusion

By understanding and utilizing the useReducer hook, you can handle complex state logic, promote code organization and improve the predictability of the state, making this hook invaluable for building scalable and maintainable components.

Top comments (0)