In a chat app I am building I want to deduct credits from a user’s account, whenever the users sends a message and when a chat is initiated.
The user account is accessible in the app as a context and uses a snapshot listener on a firestore document to update whenever something changes in the user account document. (See code samples 1. and 2. at the bottom)
Now whenever anything in the userAccount object changes, all of the context providers children (NavigationStructure and all its subcomponents) are re-rendered as per React’s documentation.
This, however causes huge problems on the chat screen that also uses this context:
The states that are defined there get re-initalized whenever something in the context changes. For example, I have a flag that indicates whether a modal is visible, default value is visible. When I go onto the chat screen, hide the modal, change a value manually in the firestore database (e.g. deduct credits) the chat screen is rerendered and the modal is visible again. (See code sample 3.)
I am very lost what the best way to solve this issue is, any ideas?
Solutions that I have thought about:
- Move the credits counter to a different firestore document and deduct the credits once per day, but that feels like a weird workaround.
- From Googling it seems to be possible to do something with useCallback or React.memo, but I am very unsure how.
- Give up and become a wood worker…seems like running away from the problem though.
- Maybe it has something to the nested react-navigation stack and tab navigators I’m using within NavigationStructure?
Desperate things I have tried:
- Wrap all sub-components of NavigationStructrue in "React.memo(..)"
- Make sure I don’t define a component within another component’s body.
- Look at loads of stack overflow posts and try to fix things, none have worked.
Code Samples
- App setup with context
function App() {
const userData = useUserData();
...
return (
<>
<UserContext.Provider value={{ ...userData }}>
<NavigationStructure />
</UserContext.Provider>
</>
}
- useUserData Hook with firestore snapshot listener
export const useUserData = () => {
const [user, loading] = useAuthState(authFB);
const [userAccount, setUserAccount] = useState<userAccount | null>();
const [userLoading, setUserLoading] = useState(true);
useEffect(() => {
...
unsubscribe = onSnapshot(
doc(getFirestore(), firebaseCollection.userAccount, user.uid),
(doc) => {
if (doc.exists()) {
const data = doc.data() as userAccount & firebaseRequirement;
//STACK OVERFLOW COMMENT: data CONTAINES 'credits' FIELD
...
setUserAccount(data);
}
...
}
);
}, [user, loading]);
...
return {
user,
userAccount,
userLoading: userLoading || loading,
};
};
- Code Sample: Chat screen with modal
export const Chat = ({ route, navigation }: ChatScreenProps): JSX.Element => {
const ctx = useContext(UserContext);
const userAccount = ctx.userAccount as userAccount;
...
//modal visibility
const [modalVisible, setModalVisible] = useState(true);
// STACK OVERFLOW COMMENT: ISSUE IS HERE.
// FOR SOME REASON THIS STATE GET'S RE-INITALIZED (AS true) WHENEVER
// SOMETHING IN THE userAccount CHANGES.
...
return (
<>
...
<Modal
title={t(tPrefix, 'tasklistModal.title')}
visible={ModalVisible}
onClose={() => setModalVisible(false)}
footer={
...
}
>
....
</Modal>
...
</>)
}
2
Answers
This is a characteristic of React Context – any change in value to a context results in a re-render in all of the context’s consumers. This is briefly touched on in the Caveats section in their docs, but is expanded on in third-party blogs like this one: How to destroy your app’s performance with React Context.
You’ve already tried the author’s suggestion of memoization. Memoizing your components won’t prevent re-initialization, since the values in the component do change when you change your user object.
The solution is to use a third-party state management solution that relies not on Context but on its own diffing. Redux, Zustand, and other popular libraries do their own comparison so that only affected components re-render.
Context is really only recommended for values that change infrequently and would require full-app re-renders anyway, like theme changes or language selection. Try replacing it with a "real" state management solution instead.
Any change to the context does indeed rerender all consumer components whether they use the changed property or not.
But it will not unmount and mount the component which is the reason why your local state gets initialized to the default value.
So the problem is not the in the rerenders (rarely the case) but rather
<Chat ... />
or one of it’s parent component unmounting due to changes in the context.It is hard to tell from the partial code examples given but I would suggest looking at how you use
loading
. Something likeloading ? <div>loading..</div> : <Chat ... />
would cause this behaviour.As an example here is a codesandbox which illustrates the points made.