Home > front end >  State does not update its value using AsyncStorage in react-native
State does not update its value using AsyncStorage in react-native

Time:05-22

I am trying to save a value to async storage and then navigate to the right page depending on what the value outcome is from the Async storage. I can store data in AsyncStorage but my states does not update, I have to reload the app in order for the state to update. here is my code:

Here I have a Welcome/Obnoarding screen. I want this screen to only show to the new app users. So when a user presses the continue button I want to save a value to the Async storage so that the next time they log in they don't have to see the onboarding page again. Here is my Onboarding page:

const WelcomeScreen: FC<IWelcomeScreen> = ({ navigation }) => {
  const { width, height } = Dimensions.get("window");

  const btnText = "Contiunue";
  const title = "Book";
  const subTitle = "Fab";

  let [fontsLoaded] = useFonts({
    PinyonScript_400Regular,
  });

  const continueBtn = async () => {
    try {
      await AsyncStorage.setItem('@viewedOnboarding', 'true');
    } catch (error) {
      console.log('Error @setItem: ', error);
    };
  };

  if (!fontsLoaded) {
    return <Text>...Loading</Text>;
  } else {
    return (
      <View style={containerStyle(height, width).container}>
        <ImageBackground
          resizeMode={"cover"}
          style={styles.image}
          source={require("../assets/model.jpg")}
        >
          <LinearGradient
            colors={["#00000000", "#000000"]}
            style={styles.gradient}
          >
            <View style={styles.container}>
              <View style={styles.logoTextContainer}>
                <Text style={styles.logoText}>{title}</Text>
                <Text style={styles.logoText}>{subTitle}</Text>
              </View>

              <ContinueBtn label={btnText} callback={continueBtn} />
            </View>
          </LinearGradient>
        </ImageBackground>
      </View>
    );
  }
};

In my AppNavigator I want to decide which navigation the user should see. But when I press the continue page my app does not navigate to my TabsNavigator. It stays on my Onboarding page but if I refresh the app then the app navigates to my Tabs navigator. here is the code where I determine where the user should be depending if they are a new user or a "old" user:

const WelcomeScreen: FC<IWelcomeScreen> = ({ navigation }) => {
  const { width, height } = Dimensions.get("window");

  const btnText = "Contiunue";
  const title = "Book";
  const subTitle = "Fab";

  let [fontsLoaded] = useFonts({
    PinyonScript_400Regular,
  });

  const continueBtn = async () => {
    try {
      await AsyncStorage.setItem('@viewedOnboarding', 'true');
    } catch (error) {
      console.log('Error @setItem: ', error);
    };
  };

  if (!fontsLoaded) {
    return <Text>...Loading</Text>;
  } else {
    return (
      <View style={containerStyle(height, width).container}>
        <ImageBackground
          resizeMode={"cover"}
          style={styles.image}
          source={require("../assets/model.jpg")}
        >
          <LinearGradient
            colors={["#00000000", "#000000"]}
            style={styles.gradient}
          >
            <View style={styles.container}>
              <View style={styles.logoTextContainer}>
                <Text style={styles.logoText}>{title}</Text>
                <Text style={styles.logoText}>{subTitle}</Text>
              </View>

              <ContinueBtn label={btnText} callback={continueBtn} />
            </View>
          </LinearGradient>
        </ImageBackground>
      </View>
    );
  }
};

CodePudding user response:

Setting a value in the async storage will not trigger a rerender of your AppNavigator. Thus, if the user presses the continue button, then nothing will happen visually, since the state of AppNavigator has not changed. If you refresh the app, the flag, which you have set previously using the setItem function, will be reloaded in AppNavigator on initial rendering. This is the reason why it works after refreshing the application.

For this kind of problem, I would suggest that you use a Context for triggering a state change in AppNavigator.

Here is a minimal example on how this would work. I have added comments in the code to guide you.

For the sake of simplicity, we will make the following assumption:

We have two screens in a Stack, one is the WelcomeScreen, the other one is called HomeScreen.

Notice that we use conditional rendering for the screens depending on our application context. You can add whatever screens you want, even whole navigators (this would be necessary if your navigators are nested, but the pattern stays the same).

App

export const AppContext = React.createContext()

const App = () => {
  // it is important that the initial state is undefined, since
  // we need to wait for the async storage to return its value 
  // before rendering anything
  const [hasViewedOnboarding, setHasViewedOnboarding] = React.useState()

  const appContextValue = useMemo(
    () => ({
      hasViewedOnboarding,
      setHasViewedOnboarding,
    }),
    [hasViewedOnboarding]
  )

  // retrieve the onboarding flag from the async storage in a useEffect
  React.useEffect(() => {
       const init = async () => {
          const value = await AsyncStorage.getItem('@viewedOnboarding')
          setHasViewedOnboarding(value != null ? JSON.parse(value) : false)
       }
       init()
  }, [])

  // as long as the flag has not been loaded, return null
  if (hasViewedOnboarding === undefined) {
    return null
  }

  // wrap everything in AppContext.Provider an pass the context as a value
  return (
      <AppContext.Provider value={appContextValue}>
        <NavigationContainer>
           <Stack.Navigator>
             {!hasViewedOnboarding ? (
                <Stack.Screen name="Welcome" component={WelcomeScreen} />
              ) : (
                <Stack.Screen
                  name="Home"
                  component={HomeScreen}
                />
              )}}
           </Stack.Navigator>
        </NavigationContainer>
     </AppContext.Provider>
  )
}

Now, in your WelcomeScreen you need to access the context and set the state after the async value has been stored.

const WelcomeScreen: FC<IWelcomeScreen> = ({ navigation }) => {
  
  // access the context

  const { setHasViewedOnboarding } = useContext(AppContext)
   
  const { width, height } = Dimensions.get("window");

  const btnText = "Contiunue";
  const title = "Book";
  const subTitle = "Fab";

  let [fontsLoaded] = useFonts({
    PinyonScript_400Regular,
  });

  const continueBtn = async () => {
    try {
      await AsyncStorage.setItem('@viewedOnboarding', 'true');
      setHasViewedOnboarding(true)
    } catch (error) {
      console.log('Error @setItem: ', error);
    };
  };

  if (!fontsLoaded) {
    return <Text>...Loading</Text>;
  } else {
    return (
      <View style={containerStyle(height, width).container}>
        <ImageBackground
          resizeMode={"cover"}
          style={styles.image}
          source={require("../assets/model.jpg")}
        >
          <LinearGradient
            colors={["#00000000", "#000000"]}
            style={styles.gradient}
          >
            <View style={styles.container}>
              <View style={styles.logoTextContainer}>
                <Text style={styles.logoText}>{title}</Text>
                <Text style={styles.logoText}>{subTitle}</Text>
              </View>

              <ContinueBtn label={btnText} callback={continueBtn} />
            </View>
          </LinearGradient>
        </ImageBackground>
      </View>
    );
  }
};
  • Related