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
BottomTabNavigator
component while doing so - This BottomTabNavigator is rendered in the root stack screen
- The context passed to the
Provider
is stored in local state in theApp
component withuseState
- Inside
Flashcards
when you click the button it callshandlePress
which calls the context'ssetWholeWordList
function, updating the context - In this case, the context is a local state in the
App
component, so it updates that state value and triggers a re-render inApp
- During re-rendering, it creates a brand new
BottomTabNavigator
component in memory and that one gets rendered - Since technically that
BottomTabNavigator
is 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