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.
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
CodePudding user response:
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.