I've been trying to update a global state used by several screens on my app using react context, which seemed to be the advice I found here.
However every time the context is updated in the screen, it ends up unmounting and mounting again. How do I prevent this?
Link to sandbox. If you click the button you will see a new console log from my useEffect.
Code below:
import React, {
useContext,
useState,
useEffect,
createContext,
Dispatch,
SetStateAction
} from "react";
import { Button } from "react-native";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { NavigationContainer } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack";
export type FlashcardWithScore = {
frontSide: string;
backSide: string;
score: number;
};
interface WordListContextInterface {
wholeWordList: FlashcardWithScore[];
setWholeWordList: Dispatch<SetStateAction<FlashcardWithScore[]>>;
}
const wordListContext = createContext<WordListContextInterface | null>(null);
const Flashcards = () => {
const appContext = useContext(wordListContext);
if (!appContext) return null;
const { wholeWordList, setWholeWordList } = appContext;
useEffect(() => {
console.log("component has been mounted");
}, []);
const handlePress = () => {
setWholeWordList([
...wholeWordList,
{ frontSide: "foo", backSide: "bar", score: 0 }
]);
};
return (
<>
<Button title="press me to see issue!" onPress={handlePress} />
</>
);
};
export default function App() {
const [wholeWordList, setWholeWordList] = useState<FlashcardWithScore[]>([
{ frontSide: "more", backSide: "edits", score: 0 }
]);
const RootStack = createStackNavigator();
const BottomTab = createBottomTabNavigator();
const BottomTabNavigator = () => {
return (
<BottomTab.Navigator initialRouteName="Learn">
<BottomTab.Screen
name="Learn"
component={Flashcards}
options={{
title: "Flashcard Learn"
}}
/>
</BottomTab.Navigator>
);
};
return (
<wordListContext.Provider value={{ wholeWordList, setWholeWordList }}>
<NavigationContainer>
<RootStack.Navigator>
<RootStack.Screen name="Root" component={BottomTabNavigator} />
</RootStack.Navigator>
</NavigationContainer>
</wordListContext.Provider>
);
}
CodePudding user response:
It's because the definition of the BottomTabNavigator is not persisted between re-renders of the App component. Following the logic through from App render to button click to re-render:
- App renders, creating a new
BottomTabNavigatorcomponent while doing so - This BottomTabNavigator is rendered in the root stack screen
- The context passed to the
Provideris stored in local state in theAppcomponent withuseState - Inside
Flashcardswhen you click the button it callshandlePresswhich calls the context'ssetWholeWordListfunction, updating the context - In this case, the context is a local state in the
Appcomponent, so it updates that state value and triggers a re-render inApp - During re-rendering, it creates a brand new
BottomTabNavigatorcomponent in memory and that one gets rendered - Since technically that
BottomTabNavigatoris a different one, React thinks it's a completely separate component, so "old" one is unmounted and this "new" one is mounted
The way to fix this is to ensure the BottomTabNavigator doesn't unnecessarily change between renders. One way is to, just like Flashcards, move it out of App into its own separate component definition. Another way is to memoize it with useCallback, i.e.
const BottomTabNavigator = useCallback(() => {
return (
<BottomTab.Navigator initialRouteName="Learn">
<BottomTab.Screen
name="Learn"
component={Flashcards}
options={{
title: "Flashcard Learn"
}}
/>
</BottomTab.Navigator>
);
}, []);
This means it won't get recreated in memory when App re-renders
