2

I have an odd situation where a state variable is updating successfully in one component but not in the other. I'm successfully changing the state, because I can see it reflected in the component where I'm triggering a dispatch. But in another component that is wrapped in the same Provider, I see no changes.

Other Stack Overflow answers seem to mostly recommend not directly mutating state, but here I'm not doing that - I'm dispatching an action to update the state, similar to what you'd see in Redux syntax.

CodeSandbox Demo

TeaSettingsContext.tsx

const TeaSettingsContext = createContext<{ state: State; dispatch: Dispatch } | undefined>(undefined);

const teaSettingsReducer = (state: State, action: Action) => {
  switch (action.type) {
    case TeaSettingsActions.ChooseTea: {
      return { ...state, chosenTea: action.payload };
    }
    case TeaSettingsActions.ChangeStrength: {
      return { ...state, desiredStrength: action.payload };
    }
    default: {
      throw new Error(`Unhandled action type: ${action.type}`);
    }
  }
};

export function TeaSettingsProvider({
  children,
}: TeaSettingsProviderProps): Context {
  const [state, dispatch] = useReducer(teaSettingsReducer, {
    chosenTea: {},
    desiredStrength: 0.5,
  });
  return (
    <TeaSettingsContext.Provider value={{ state, dispatch }}>
      {children}
    </TeaSettingsContext.Provider>
  );
}

export const useTeaSettingsContext = (): TeaSettingsContext => {
  const context = useContext(TeaSettingsContext);
  if (context === undefined) {
    return;
  }
  return context;
};

Here's the "successful" component:

Home.tsx

<TeaSettingsProvider>
  <StrengthSlider />
</TeaSettingsProvider>

StrengthSlider.tsx

export default function StrengthSlider(): ReactNode {
  const { state, dispatch } = useTeaSettingsContext();
  console.log(state.desiredStrength) // logs the increment on each press.

  const incrementStrength = () => {
    dispatch({
      payload: state.desiredStrength + 0.1,
      type: "change-strength",
    });
  };

  return (
    <Box position="relative" w="100%">
      <Button title="Press" onPress={incrementStrength} />
      <Text>{state.desiredStrength}</Text>
    </Box>
  );
}

...and the unsuccessful re-render happens in this component:

TeaPage.tsx

const Component = () => {
    if (type === "tea") {
      return data.map((teaObj) => (
        <TeaCard id={teaObj.id} teaData={teaObj.data} key={teaObj.id} />
      ));
    }
  };

  return (
    <TeaSettingsProvider>
      <Component />
    </TeaSettingsProvider>
  );

TeaCard.tsx

export function TeaCard({ id, teaData }: TeaCardProps): ReactNode {
  const { state, dispatch } = useTeaSettingsContext();
  console.log(state.desiredStrength); // this always logs the starting value of 0.5 and doesn't log each time I press the button above.
// ...
}

FYI: Based my code on Kent C Dodds' article: https://kentcdodds.com/blog/how-to-use-react-context-effectively

crevulus
  • 1,658
  • 12
  • 42

1 Answers1

1

I think you misunderstood a little how context works.

Basically (as react docs suggest):

Every Context object comes with a Provider React component that allows consuming components to subscribe to context changes.

The Provider component accepts a value prop to be passed to consuming components that are descendants of this Provider. One Provider can be connected to many consumers. Providers can be nested to override values deeper within the tree.

All consumers that are descendants of a Provider will re-render whenever the Provider’s value prop changes. The propagation from Provider to its descendant consumers (including .contextType and useContext) is not subject to the shouldComponentUpdate method, so the consumer is updated even when an ancestor component skips an update.

On short when you create a provider with a value you are able to access that information from multiple consumers (that need to be children of that provider). So far so good.

Except: Home.tsx and TeaPage.tsx both initialize a different instance of the context (so each one has it's own provider). In order to fix this you should only create one provider in a component that is parent of both Home and TeaPage.

React context is not working as redux (where you have a global store that you can access from everywhere). It rather give you the option to create a context (with reusable data) for a big component (to consume in the children).

Also please notice the last quoted paragraph from the beginning: basically when the context changes this cause all consumers to re-render (which might sometimes lead to some performance issues) so use it wisely. Here is a short article about this.

Berci
  • 2,876
  • 1
  • 18
  • 28
  • Ah okay. I originally had it all in `AppContext` that kind of provided a global state, but caused re-rendering and performance issues because other state values (i.e. not the ones in this context provider) were large and contained images etc.. So could I do something like wrap my `Routes` component (which contains both `Home` and `TeaPage`) in the `TeaSettingsProvider`? Then it should update `desiredStrength` and `chosenTea` without affecting the other variables that are large and slow-to-load, right? – crevulus Oct 22 '21 at 16:57
  • Yes. Additionally: if you need multiple values in different places is better to create multiple separate context to prevent unnecessary re-rendering. Also in a specific case I ended up moving some data to the store and some in normal states to improve performance. So keep an eye out for this too in general. – Berci Oct 22 '21 at 17:29
  • But if you have a global state and no reason to avoid using it for this case I would rather go for global state in your case. This is not mandatory and context should work. But I think context is suited rather for updating the hole app (like changing the language or the theme), or for a specific large component with lots of children that need to use a context and is not worth to keep in global state since is for a very specific part of the code. You can also read in the provided react docs their recommendation (at the beginning of the article) – Berci Oct 22 '21 at 17:35