Difference between useState and useReducer

Both useState and useReducer are React hooks that allow you to manage state in your functional components. However, they have different advantages and disadvantages depending on the complexity and structure of your state.

const [state, setState] = useState(initialState);
const [state, dispatch] = useReducer(reducer, initialArg, init?)

useState is simpler and more lightweight, making it suitable for basic state management. It lets you declare a state variable and a function to update it in one line of code. You can use multiple useState hooks to manage different pieces of state in your component. useState is also easy to understand and implement, even for beginners.

useReducer, on the other hand, is more complex but offers more control over state and is better suited for handling complex state changes or actions. It allows you to handle multiple actions within a single function rather than writing separate functions for each action with useState. This makes it ideal for larger applications with greater complexity when managing state changes across components.

useReducer also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks. This means that the dispatch function does not change between renders, unlike the functions created by useState, which can cause unnecessary re-renders of child components.

useState

useState is a React hook that lets you add a state variable to your component. It returns an array with two values: the current state and a function to update it.

import { useState } from 'react'

function MyComponent() {
  // declare a state variable called count and a function to update it
  const [state, setState] = useState(initialState)

  // ...
}
  • state is the current value of the state variable. It can be any type of data, such as a number, a string, an object, or an array.
  • setState is a function that lets you change the state value and trigger a re-render of the component. You can pass it a new value directly, or a function that returns the new value based on the previous state.
  • initialState is the initial value of the state variable. It can be any type of data, or a function that returns the initial value. The function is only called once during the first render.

Here’s an example, Counter uses the useState hook to declare a state variable called count and a function to update it. The initial value of the count is set to 0. The component also defines two functions to increment and decrement the count by one, using the setCount function.

import { useState } from 'react'

function Counter() {
  // declare a state variable called count and a function to update it
  const [count, setCount] = useState(0);

  // define a function to increment the count by one
  function increment() {
    setCount((c) => c + 1);
  }

  // define a function to decrement the count by one
  function decrement() {
    setCount((c) => c - 1);
  }

  // return the JSX to render the component
  return (
    <div>
      <h1>Counter</h1>
      <p>The current count is {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
}

You can use multiple useState hooks to manage different pieces of state in your component:

function MyComponent() {
  // declare a state variable called name and a function to update it
  const [name, setName] = useState('John')

  // declare a state variable called age and a function to update it
  const [age, setAge] = useState(25)

  // ...
}

You can also use useState to manage complex state objects or arrays. However, you need to be careful when updating them, because useState does not merge the new state with the old state like this.setState does in class components. Instead, it replaces the old state with the new state. Therefore, you need to spread the old state and overwrite only the properties that you want to change.

function MyComponent() {
  // declare a state variable called user and a function to update it
  const [user, setUser] = useState({
    name: 'John',
    age: 25,
    email: 'john@example.com'
  })

  // define a function to change the user's name
  function changeName(newName) {
    // spread the old user state and overwrite the name property
    setUser({
      ...user,
      name: newName
    })
  }

  // ...
}

useReducer

useReducer is a React hook that lets you add a reducer to your component. A reducer is a function that specifies how the state gets updated based on an action. It is similar to the concept of reducers in Redux, but it is local to your component and does not require any external libraries.

import { useReducer } from 'react'

function MyComponent() {
  // declare a reducer function that handles different actions
  function reducer(state, action) {
    switch (action.type) {
      // TODO
    }
  }

  // declare a state variable and a dispatch function using useReducer
  const [state, dispatch] = useReducer(reducer, initialArg, init)

  // ...
}
  • state is the current value of the state variable. It can be any type of data, such as a number, a string, an object, or an array.
  • dispatch is a function that lets you dispatch an action to the reducer and trigger a state update and a re-render. An action can be any type of data, but it usually has a type property that indicates the type of action and an optional payload property that carries some data.
  • reducer is a function that takes the current state and an action as arguments and returns the next state. It must be pure, meaning it does not mutate the state or cause any side effects.
  • initialArg is the value from which the initial state is calculated. It can be any type of data, or a function that returns the initial value.
  • init is an optional function that takes the initialArg as an argument and returns the initial state. It is useful when you want to perform some calculations or transformations on the initialArg before setting the initial state.

Here’s an example reimplementing above Counter component uses the useReducer hook to declare a state variable called count and a dispatch function. The initial value of the count is set to 0. The component also defines a reducer function that handles two types of actions: ‘increment’ and ‘decrement’. The reducer function returns the next state based on the action type and the current state. The component also defines two functions to increment and decrement the count by one, using the dispatch function.

import { useReducer } from 'react'

function Counter() {
  // declare a reducer function that handles different actions
  function reducer(state, action) {
    switch (action.type) {
      case 'increment':
        return state + 1
      case 'decrement':
        return state - 1
      default:
        return state
    }
  }

  // declare a state variable and a dispatch function using useReducer
  const [count, dispatch] = useReducer(reducer, 0)

  // define a function to increment the count by one
  function increment() {
    dispatch({ type: 'increment' })
  }

  // define a function to decrement the count by one
  function decrement() {
    dispatch({ type: 'decrement' })
  }

  // return the JSX to render the component
  return (
    <div>
      <h1>Counter</h1>
      <p>The current count is {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  )
}

Redux vs useReducer

Redux and useReducer are both state management solutions that use the concept of reducers to update the state based on actions. However, they have some key differences that make them suitable for different scenarios and preferences.

  • useReducer is a built-in React hook that lets you add a reducer to your component, while Redux is an external library that creates a global state container for your whole application.
  • useReducer is tightly coupled to a specific reducer function that handles the actions dispatched by the component, while Redux dispatches actions to the store, which can have multiple reducers combined by combineReducers.
  • useReducer returns the current state and a dispatch function to the component, while Redux requires you to use connect or useSelector and useDispatch hooks to access the state and dispatch actions from the component.
  • useReducer is simpler and more lightweight than Redux, but it also has less features and tools. For example, Redux has devtools, middleware, selectors, thunks, sagas, and more.
  • useReducer is easier to learn and implement than Redux, but it also has some limitations and challenges. For example, useReducer can cause performance issues if used with useContext, or it can be difficult to share state between components or test the reducer logic.

In general, Redux is more suitable for large and complex applications that need a single source of truth for the state, advanced state management features, and a rich ecosystem of tools and plugins. Here are the steps to implement a counter using Redux in React:

  1. Install Redux and React-Redux as dependencies in your project:
npm install redux react-redux
  1. Create a file called counterReducer.js and define the reducer function for the counter state. The reducer function takes the current state and an action as arguments and returns the next state based on the action type. The initial state is set to 0.
// counterReducer.js

// define the action types as constants
const INCREMENT = 'INCREMENT'
const DECREMENT = 'DECREMENT'

// export the action types for other files to use
export { INCREMENT, DECREMENT }

// define the reducer function for the counter state
function counterReducer(state = 0, action) {
  switch (action.type) {
    case INCREMENT:
      return state + 1
    case DECREMENT:
      return state - 1
    default:
      return state
  }
}

// export the reducer function for other files to use
export default counterReducer
  1. Create a file called store.js and create the Redux store using createStore from Redux. The store holds the global state of the application and allows components to dispatch actions and subscribe to state changes. Pass the counterReducer as an argument to createStore.
// store.js

import { createStore } from 'redux'
import counterReducer from './counterReducer'

// create the Redux store using createStore from Redux
const store = createStore(counterReducer)

// export the store for other files to use
export default store
  1. Create a file called Counter.js and define a functional component that renders the counter state and two buttons to increment and decrement it. Use useSelector and useDispatch hooks from React-Redux to access the state and dispatch actions from the component. Use the action types defined in counterReducer.js to dispatch the correct actions.
// Counter.js

import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { INCREMENT, DECREMENT } from './counterReducer'

function Counter() {
  // use useSelector hook from React-Redux to get the counter state from the store
  const count = useSelector((state) => state)

  // use useDispatch hook from React-Redux to get a reference to the dispatch function
  const dispatch = useDispatch()

  // define a function to increment the count by one
  function increment() {
    dispatch({ type: INCREMENT })
  }

  // define a function to decrement the count by one
  function decrement() {
    dispatch({ type: DECREMENT })
  }

  // return the JSX to render the component
  return (
    <div>
      <h1>Counter</h1>
      <p>The current count is {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  )
}

export default Counter
  1. Create a file called App.js and define a functional component that renders the Counter component inside a Provider component from React-Redux. The Provider component allows you to pass the store to any component that needs it without explicitly passing it as props.
// App.js

import React from 'react'
import { Provider } from 'react-redux'
import store from './store'
import Counter from './Counter'

function App() {
  // return the JSX to render the component
  return (
    // wrap the Counter component inside a Provider component from React-Redux
    // pass the store as a prop to the Provider component
    <Provider store={store}>
      <Counter />
    </Provider>
  )
}

export default App

React state management

State management in React is a process for managing the data that React components need in order to render themselves. This data is typically stored in the component’s state object. When the state object changes, the component will re-render itself.

React state management is basically half of a React app. It includes all the data that determines the behavior and appearance of the UI. The other half is the presentation, including the HTML, CSS, and formatting.

State and state management is relied on by the presentation part of the app. The only time a React app will re-render is when state changes.

There are various ways to effectively apply state management in React apps, depending on the complexity and structure of the state, and the needs and preferences of the developers. Some of the common ways to manage state in React are:

  • Component state: This is the simplest way to manage state in React. It uses the useState or useReducer hooks to declare and update state variables within functional components, or this.setState method within class components. Component state is local to each component and can be passed down to child components as props.

  • Context API: This is a built-in feature of React that allows you to pass data to any component without explicitly passing it as props. It uses the useContext hook to access the data from a Provider component that wraps the component tree. Context API is useful for sharing global or common data across multiple components, such as themes, user information, or authentication status.

  • React hooks: These are built-in functions that let you use state and other React features without writing a class component. They include useState, useReducer, useEffect, useMemo, useCallback, useRef, and more. React hooks allow you to manage state and side effects in a declarative and functional way.

  • High-order components: These are functions that take a component as an argument and return a new component that wraps the original one. They are used to share common logic or data between components without repeating code. For example, you can use a high-order component to inject props, handle authentication, or connect to Redux store.

  • React state management libraries: These are external libraries that provide more advanced and robust solutions for state management in React. They include Redux, Recoil, MobX, Zustand, XState, and more. They usually have their own concepts and APIs for creating a global or local state container, dispatching actions, updating state, subscribing to state changes, and optimizing performance.