Can useReducer+createContext really replace Redux?

Concept

useReducer

useReducer is a state management hook provided by React, usually used to manage complex state logic of components. It takes two parameters: the reducer function and the initial state. The Reducer function accepts the current state and an action and returns a new state. This is somewhat similar to the reducer function in Redux. Use useReducer to more clearly manage the component’s state update logic, which is especially suitable for situations where multiple related states are processed or complex calculations need to be performed. Can be used as an alternative to useState in some scenarios.

How to use:

const [state, dispatch] = useReducer(reducer, initialState)

It returns the current state state, and dispatch method. When we need to change the state, call the dispatch method:

dispatch({<!-- -->type: 'increment'})

This triggers the reducer function, which returns the new state value.

Example: Counter

//Define the reducer function, accept the current state and operation (action), and return the new state
const counterReducer = (state, action) => {<!-- -->
  switch(action.type) {<!-- -->
    case 'increment':
      return {<!-- -->count: state.count + 1}
    case 'decrement':
      return {<!-- -->count: state.count - 1}
    default:
      return state
  }
}

// counter function
function Counter() {<!-- -->
// Use the useReducer hook, passing in the reducer function and initial state
  const [state, dispatch] = useReducer(counterReducer, {<!-- -->count: 0})

  return (
    <div>
      <button onClick={<!-- -->() => dispatch({<!-- -->type: 'increment'})}> + </button>
      <span>{<!-- -->state.count}</span>
      <button onClick={<!-- -->() => dispatch({<!-- -->type: 'decrement'})}>-</button>
    </div>
  )
}

In this way, the state management logic can be abstracted through useReducer, making the component clearer.

useContext

useContext is a hook provided by React for accessing data in the context in function components. Context is a way to pass data across the component tree, which avoids the trouble of passing state layer by layer through the props of intermediate components. , especially useful for sharing global state or application-wide configuration information.
Here is an example of global theme color

import React, {<!-- --> createContext, useContext, useState } from 'react';

//Create a context object
const ThemeContext = createContext();

function App() {<!-- -->
  const [theme, setTheme] = useState('light');

  return (
    // Use ThemeContext.Provider to provide data
    <ThemeContext.Provider value={<!-- -->theme}>
      <div>
        <Header />
        <Main />
      </div>
      <button onClick={<!-- -->() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
    </ThemeContext.Provider>
  );
}

function Header() {<!-- -->
  // Use useContext hook to access data in the context
  const theme = useContext(ThemeContext);

  return (
    <header style={<!-- -->{<!-- --> backgroundColor: theme === 'dark' ? 'black' : 'white' }}>
      Header Content
    </header>
  );
}

function Main() {<!-- -->
  // Use useContext hook to access data in the context
  const theme = useContext(ThemeContext);

  return (
    <main style={<!-- -->{<!-- --> color: theme === 'dark' ? 'white' : 'black' }}>
      Main Content
    </main>
  );
}

export default App;

In this example, a ThemeContext context object is created, and then a data named theme is provided in the App component using ThemeContext.Provider. This data represents the current topic.

In the Header and Main components, we use the useContext hook to access the theme data in the ThemeContext context. This allows these two components to obtain theme data without passing props one level at a time, and when the theme changes, these two components will automatically update.

Finally, the “Toggle Theme” button in the App component allows us to switch between light and dark themes because the theme state is managed in the App component and passed to the Header and Main components through the context.

useReducer + createContext example

Using React’s useReducer and Context is a powerful way to manage the global state of your application, especially when state needs to be shared between multiple components. It avoids passing props between components and has clearer state management. useReducer extracts state logic and makes state changes more predictable. context extracts state outside the component tree and decouples it from business logic.
Below is a basic example showing how to use these two features to extend your application.

First, create a global state manager and context. This context will contain the state and functions for updating the state:

import React, {<!-- --> createContext, useReducer, useContext } from 'react';

//Create an initial state
const initialState = {<!-- -->
  count: 0,
};

// Create a reducer function to handle status updates
function reducer(state, action) {<!-- -->
  switch (action.type) {<!-- -->
    case 'INCREMENT':
      return {<!-- --> count: state.count + 1 };
    case 'DECREMENT':
      return {<!-- --> count: state.count - 1 };
    default:
      return state;
  }
}

//Create context
const AppContext = createContext();

//Create the Provider component of the context
export function AppProvider({<!-- --> children }) {<!-- -->
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <AppContext.Provider value={<!-- -->{<!-- --> state, dispatch }}>
      {<!-- -->children}
    </AppContext.Provider>
  );
}

// Custom hook for accessing context
export function useAppContext() {<!-- -->
  return useContext(AppContext);
}

In the above code, a state manager is created, which contains a state object and a reducer function to handle state updates. Then, a context is created using createContext and a component named AppProvider is created that provides state and dispatch functions to the context. Finally, a custom hook useAppContext is created to access the context.

Next, you can use this context and state manager in your application. Here is an example component demonstrating how to use global state:

import React from 'react';
import {<!-- --> useAppContext } from './AppContext';

function Counter() {<!-- -->
  const {<!-- --> state, dispatch } = useAppContext();

  return (
    <div>
      <p>Count: {<!-- -->state.count}</p>
      <button onClick={<!-- -->() => dispatch({<!-- --> type: 'INCREMENT' })}>Increment</button>
      <button onClick={<!-- -->() => dispatch({<!-- --> type: 'DECREMENT' })}>Decrement</button>
    </div>
  );
}

function App() {<!-- -->
  return (
    <div>
      <h1>My App</h1>
      <Counter />
    </div>
  );
}

export default App;

In this example, the useAppContext hook is imported and then used in the Counter component to obtain global state and dispatch functions. The Counter component can access global state and trigger updates to the state, which will be reflected throughout the application.

Finally, use AppProvider in your app’s root component to make global state available throughout your app:

import React from 'react';
import {<!-- --> AppProvider } from './AppContext';
import App from './App';

function Root() {<!-- -->
  return (
    <AppProvider>
      <App />
    </AppProvider>
  );
}

export default Root;

Summary

useReducer + createContext can indeed replace Redux in some scenarios, but it cannot really replace it. It is just that redux can be eliminated in some scenarios. When choosing which one to use, first base it on your own habits and project needs.
For example:

  1. State management complexity

If the state logic is very complex, shared by multiple components, and contains a large amount of business logic, Redux’s centralized state management is still necessary. useReducer + createContext is suitable for small and medium-sized state management.

  1. Middleware requirements

Redux’s powerful middleware (such as Redux Thunk, Saga) can solve more scenarios, but useReducer’s middleware support is weak.

  1. Debugging experience

Redux Devtools provides a better debugging experience. useReducer needs to be implemented by yourself.

  1. TypeScript integration

Redux is more friendly to TypeScript support.

  1. Project scale

In large projects, Redux does a better job of being more structured and predictable.

Therefore, whether to use Redux or HOOKS still depends on the team size, project complexity and other specific circumstances. But in some small and medium-sized projects, useReducer + createContext can be used as a simple and effective state management solution.