reactjsionic-frameworkreact-hooksuse-reducerionic-react

useReducer in Context consumer does not update after change in some locations


I added a context that contains a useReducer hook to my Ionic React app. I'm seeing some strange behavior: when I update the context value with a dispatch call, then a consumer component will be updated on the page, but the exact same component on the tab bar does not get updated.

I followed this tutorial.

When I add console.log statements to check whether the components are being reloaded, I see that the component placed in the tab bar (<TabBarCounter>) is not being reloaded even though the context value has changed.

When I add console.log statement to check for re-rendering in my context provider, I see that it doesn't get re-rendered when a dispatch is called, either.

It seems like the context is being updated locally rather than globally. There is a comment in this answer:

You are updating your state correctly using a reducer but it will only update local component state not the global context state.

That sounds a lot like the problem I am having here.

Here's some code:

MyContext.tsx

export const CountContext = React.createContext<any>({} as {
  countState: CountState,
  countDispatch: React.Dispatch<CountReducerActions>,
});

interface MyProps {
  children: JSX.Element,
}

const reducer = (countState: CountState, action: CountReducerActions) => {
  switch (action.type) {
    case 'add1': {
      countObject.total += 1;
      return countObject;
    }
    default: {
      throw new Error();
    }
  }
};

export const CountContextProvider: React.VFC<MyProps> = ({ children }: MyProps) => {
    const [countState, countDispatch] = useReducer(
    reducer,
    {
      total: 0,
    },
  );

  return (
    <CountContext.Provider value={{ countState, countDispatch }}>
      {children}
    </CountContext.Provider>
  );
};

how I update the context

const { countState, countDispatch } = useContext(CountContext);
countDispatch({ type: 'add1' });

MyComponentThatDoesNotGetRerendered.tsx

import React, { useContext } from 'react';
import { IonBadge } from '@ionic/react';
import { CountContext } from '../../context/CountContext';

const TabBarCounter: React.VFC = () => {
  const [countState] = useContext(CountContext);
  return (
    <IonBadge>
      {countState.total}
    </IonBadge>
  );
};

export default TabBarCounter;

Router.tsx

<CountContextProvider>
  <IonReactRouter>
    <AppTabBar>
      <IonRouterOutlet>
        <Route exact path={myPageRoute}>
          <MyPage />
        </Route>
        <Route>
          <PageError404 />
        </Route>
      </IonRouterOutlet>
    </AppTabBar>
  </IonReactRouter>
</CountContextProvider>

AppTabBar.tsx

const AppTabBar: React.VFC<MyProps> = ({ children }: MyProps) => {
  const [userObject] = useContext(UserContext);
  return (
    <IonTabs>
      {children}
      <IonTabBar slot="bottom" id="appTabBar">
        <IonTabButton tab="tab-settings" href={routeTabSettings}>
          <IonLabel>Settings</IonLabel>
        </IonTabButton>
        <IonTabButton
          tab="tab-count"
          href={routeTabCount}
        >
          <TabBarReviewCounter />
          <IonLabel>Count</IonLabel>
        </IonTabButton>
      </IonTabBar>
    </IonTabs>
  );
};

In this case, when the context is updated in <MyPage>, the <TabBarCounter> that is inside <AppTabBar> does not get updated, but the <TabBarCounter> inside <MyPage> does get updated.

How do I update the context correctly using useReducer() so that when I update the context value, all the consumers of that context get updated?


Solution

  • Take a look at your reducer. Instead of modifying state in immutable way you simply overwrite property without creating new reference, therefore context value never updates.

    Some components may 'see' this change when they get rerendered because of some reason - local state change, prop change etc. They will reach context, look into provided object and see new value.

    To fix it use spread operator to create new objects with keys from previous state and updated total property.

    case 'add1': {
      return {
        ...countObject,
        total: countObject.total + 1,
      };
    }