Using useReducer and useContext, the component is not rerendered when an action is dispatched. How to trigger the rerender?

Viewed 138

I am a beginner with React and I am studying how to work with useReducer and useContext together. The very simple study example I am working on is just a component called Greeting that takes the state passed from the StateContext, it displays the name of the user (Mike) from the Main.js and it has a button that should dispatch the action to change the name into John. The problem is that when the button is clicked nothing happens and the name in the greeting component stays the same. I noticed that the console.log inside the Greeting component is not triggered once the action changeName is dispatched, meaning, I believe, that the Greeting component is not rerendered. Why the Greeting is not rerendering and showing the updated name

This is the Main component:

import React, { useReducer } from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';

// components
import StateContext from './components/StateContext';
import DispatchContext from './components/DispatchContext';
import Greeting from './components/Greeting';

function reducer(state, action) {
  switch (action.type) {
    case 'changeName':
      const newState = state;
      newState.name = action.name;
      return newState;
    default:
      return;
  }
}

function App() {
  const [state, dispatch] = useReducer(reducer, {
    name: 'Mike',
  });
  console.log('name', state.name);
  return (
    <BrowserRouter>
      <Switch>
        <StateContext.Provider value={state}>
          <DispatchContext.Provider value={dispatch}> 
            <Route path="/greeting">
              <Greeting />
            </Route>
          </DispatchContext.Provider>
        </StateContext.Provider>
      </Switch>
    </BrowserRouter>
  );
}

export default App;

This is the Greeting component:

import React, { useContext } from 'react';
import StateContext from './StateContext';
import DispatchContext from './DispatchContext';

const Greeting = () => {
  const state = useContext(StateContext);
  const appDispatch = useContext(DispatchContext);
  console.log('inside greeting component', state.name)

  return (
    <>
      <div>{state.name}</div>
      <button onClick={() => appDispatch({ type: 'changeName', name: 'John' })}>
        Change Name
      </button>
    </>
  );
};

export default Greeting;
 

DispatchContext:

import { createContext } from 'react';

const DispatchContext = createContext();

export default DispatchContext;

StateContext:

import { createContext } from 'react'

const StateContext = createContext()

export default StateContext
1 Answers

Issue

The issue here is state object mutation.

function reducer(state, action) {
  switch (action.type) {
    case 'changeName':
      const newState = state;      // <-- newState is reference to state
      newState.name = action.name; // <-- mutation!!
      return newState;             // <-- same state reference
    default:
      return;
  }
}

When you mutate that state object and return it, it's still a reference to the previous state so React bails on rerendering it.

Solution

Return a new state object. Shallow copy the previous state into a new state object and then update nested properties. If the state update was more deeply nested then you would also need to shallow copy any nested state that is updated. Don't forget to return the current state object in the default case.

function reducer(state, action) {
  switch (action.type) {
    case 'changeName':
      return {
        ...state,
        name: action.name
      };

    default:
      return state;
  }
}
Related