Search code examples
reactjsreact-nativepromiseasyncstorage

React Native Nested AsyncStorage Call Not Rendering


I am trying to use the asyncstorage library to store and display random numbers on a react native page. Basically I want the code to display a list of random numbers, preceded by the number of items in the list, and buttons to add and remove random numbers like so: enter image description here

Almost everything is working, I can add and remove random numbers, and the UI will update properly, however, the first time the screen is rendered (e.g. when navigated to from another page), no numbers are displayed, even if there are multiple saved. I can tell they are saved properly, because the first number (amount of random numbers in list) is displayed correctly, and all of the numbers appear after pressing either button, just not on the initial render. I initially believed it had to do with the nested asyncstorage calls, but unnesting them and adding await keywords, wrapped in an async function provided the same result. Is there a fix to the method I use below, or is there a different approach I should be using to display this properly.

const [loopNum, setLoopNum] = React.useState(0);
      const [savedNumbers, setSavedNumbers] = React.useState<any>([]);
    React.useEffect(() => {
    AsyncStorage.getItem('@loopNum').then(res => {
          if (res !== undefined && res !== null && !isNaN(parseInt(res,10))){
            setLoopNum(parseInt(res,10));
            let rows = savedNumbers;
            for (let i = 0; i < parseInt(res,10); i++){
              AsyncStorage.getItem('@number' + i).then(resInner => {
                rows.push(<Text>Number {resInner}</Text>);
              })
            }
            setSavedNumbers(rows);
          }else{
            setLoopNum(0);
          }
        })
      })
    const addNum = () => {
        const value =  Math.random().toString();
        AsyncStorage.setItem('@number' + loopNum, value).then(res => {
          let _temp = savedNumbers;
          _temp.push(<Text>Number {value}</Text>);
          setSavedNumbers(_temp);
          AsyncStorage.setItem('@loopNum', (loopNum+1).toString()).then(resNum => {
            setLoopNum(loopNum+1);
          })
          
      })
    }
      const removeNum = () => {
      AsyncStorage.removeItem('@number' + loopNum).then(res => {
          let _temp = savedNumbers;
          _temp.pop();
          setSavedNumbers(_temp);
          AsyncStorage.setItem('@loopNum', (loopNum-1).toString()).then(resNum => {
            setLoopNum(loopNum-1);
          })
          
      })
    }
      return (
        <View style={styles.container}>
          <Button
            onPress={addNum}
            title="Add"
            color="#841584"/>
          <Button
            onPress={removeNum}
            title="Remove"
            color="#841584"/>
          <Text>{loopNum}</Text>
          <View>{savedNumbers}</View>
        </View>
      );

--Edit I have tried multiple approaches using async functions like suggested by @Nikita, but they have all yielded similar results to the code I have attached.

--EDIT 2 I was able to solve the issue, but I don't know why it works. To fix this, I added a new arbitrary constant:

const [testNum, setTestNum] = React.useState("");

and then added this bit of code to the end of my useeffect function:

AsyncStorage.setItem('@arbitrary',"arbitrary").then(resNum => {
      setTestNum("arbitrary");
}) 

I never use or display the testNum variable, nor do anything with the 'arbitrary' key, I am able to place anything there, and it works as long as there is a storage call and a variable update. Why does this fix my problem?


Solution

  • I had some time (and interest) to go through this. The complete solution can be found and tested in here: https://snack.expo.dev/@zvona/asyncstorage-so-problem.

    There are quite many changes, but I'll add the whole code in the bottom in case Expo Snack disappears on some day.

    Key takes:

    • you're referring to the original (stated) arrays with code like: let rows = savedNumbers;. It's not creating a new instance, but you're directly mutating the existing one. This caused some errors
    • async/await pattern really works better and produces cleaner code
    • you should separate the functional behavior and interaction with DB (AsyncStorage) from presentation. Therefore I created a completely separated function for populating the contents from storage.
    import * as React from 'react';
    import { Button, Text, View, StyleSheet } from 'react-native';
    import AsyncStorage from '@react-native-async-storage/async-storage';
    
    const App = () => {
      const [loopNum, setLoopNum] = React.useState(0);
      const [savedNumbers, setSavedNumbers] = React.useState([]);
    
      const initiate = async () => {
        const currentLoopNum = (await AsyncStorage.getItem('@loopNum')) || '0';
        const numbers = [];
    
        for (let i = 0; i < parseInt(currentLoopNum, 10); i++) {
          const savedNumber = await getSavedNumber(i);
          numbers.push(savedNumber);
        }
    
        setLoopNum(parseInt(currentLoopNum, 10));
        setSavedNumbers(numbers);
      };
    
      React.useEffect(() => {
        initiate();
      }, []);
    
      const getSavedNumber = async (i) => {
        const num = await AsyncStorage.getItem('@number' + i);
        return num;
      };
    
      const populateSavedNumbers = () => 
        savedNumbers.map((num) => <Text>{`Number: ${num}`}</Text>);
    
      const addNum = async () => {
        const value = Math.random().toString();
        const newLoopNum = loopNum + 1;
        
        await AsyncStorage.setItem(`@number${loopNum}`, value);
        await AsyncStorage.setItem('@loopNum', newLoopNum);
    
        setSavedNumbers([...savedNumbers, value]);
        setLoopNum(newLoopNum);
      };
    
      const removeNum = async () => {
        const newNumbers = savedNumbers.slice(0, -1);
        const newLoopNum = loopNum - 1;
    
        await AsyncStorage.removeItem(`@number${loopNum}`);
        await AsyncStorage.setItem('@loopNum', newLoopNum);
    
        setSavedNumbers(newNumbers);
        setLoopNum(newLoopNum);
      };
    
      return (
        <View style={styles.container}>
          <Button onPress={addNum} title="Add" color="#841584" />
          <Button onPress={removeNum} title="Remove" color="#841584" />
          <Text>{loopNum}</Text>
          <View>{populateSavedNumbers()}</View>
        </View>
      );
    };
    
    const styles = StyleSheet.create({
      container: {
        flex: 1,
        justifyContent: 'center',
        backgroundColor: '#ecf0f1',
        padding: 8,
      },
    });
    
    export default App;