Home > Back-end >  Component remounting on context update
Component remounting on context update

Time:04-03

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:

  1. App renders, creating a new BottomTabNavigator component while doing so
  2. This BottomTabNavigator is rendered in the root stack screen
  3. The context passed to the Provider is stored in local state in the App component with useState
  4. Inside Flashcards when you click the button it calls handlePress which calls the context's setWholeWordList function, updating the context
  5. In this case, the context is a local state in the App component, so it updates that state value and triggers a re-render in App
  6. During re-rendering, it creates a brand new BottomTabNavigator component in memory and that one gets rendered
  7. 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

  • Related