Search code examples
reactjsreact-nativeasyncstoragereact-native-state

Why does AsyncStorage save an empty array with the first click and then save the entered value only with the second button click in react native


I try to save the tasks in my ToDo app with AsyncStorage so that they can be retrieved after an app restart.

So far I have managed to save the tasks. However, an empty array is always saved in the first run. If I want to create a new task, it only saves it the second time I click the button. Logical if the whole thing runs asynchronously. I just can't figure out where my fault is. I would be very happy to receive help and tips.

Here you can see the empty array when creating the first task: Reactotron Empty Array

And here you can see the first value get's saved after i created the second task: Reactotron AsyncStorage after second click

First Part:

if (__DEV__) {
  import('./ReactotronConfig').then(() => console.log('Reactotron Configured'));
}
import Reactotron, { asyncStorage } from 'reactotron-react-native';
import React, { useState, useEffect } from 'react';
import {
  Keyboard,
  KeyboardAvoidingView,
  Platform,
  StyleSheet,
  Text,
  TextInput,
  TouchableOpacity,
  View,
  ScrollView,
  Image,
  SafeAreaView,
} from 'react-native';

import AsyncStorage from '@react-native-async-storage/async-storage';
import Task from './components/Task';

export default function App() {
  const [task, setTask] = useState();
  const [taskItems, setTaskItems] = useState([]);

  const getData = async () => {
    try {
      const jsonValue = await AsyncStorage.getItem('TASKS');
      const jsonValue2 = JSON.parse(jsonValue);
      if (jsonValue2 !== null) {
        setTaskItems(jsonValue2);
      }
    } catch (e) {
      alert(e);
    }
  };

  const storeData = async () => {
    await AsyncStorage.clear();
    try {
      const jsonValue = await AsyncStorage.setItem(
        'TASKS',
        JSON.stringify(taskItems)
      );
      return jsonValue;
      Reactotron.log(jsonValue);
    } catch (e) {
      alert(e);
    }
  };

  useEffect(() => {
    getData();
  }, []);

  const handleAddTask = () => {
    storeData();
    Keyboard.dismiss();
    setTaskItems([...taskItems, task]);
    setTask(null);
  };

  const completeTask = (index) => {
    let itemsCopy = [...taskItems];
    itemsCopy.splice(index, 1);
    setTaskItems(itemsCopy);
  };

  const bearyDustLogo = require('./assets/bearydust-logo-bear-with-text.png');

Second Part:

return (
    <SafeAreaView style={styles.container}>
      <ScrollView style={styles.scrollView}>
        {/* Aufgaben für heute */}

        <View style={styles.tasksWrapper}>
          <View style={styles.headerWrapper}>
            <Text style={styles.sectionTitle}>StandUp Aufgaben</Text>
            <Image style={styles.tinyLogo} source={bearyDustLogo}></Image>
          </View>
          <View style={styles.items}>
            {/* Aufgabenbereich */}
            {taskItems.map((item, index) => {
              return (
                <TouchableOpacity
                  key={index}
                  onPress={() => completeTask(index)}
                >
                  <Task text={item} />
                </TouchableOpacity>
              );
            })}
          </View>
        </View>
      </ScrollView>

      {/* Aufgabe erstellen */}

      <KeyboardAvoidingView
        behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
        style={styles.writeTaskWrapper}
      >
        <TextInput
          style={styles.input}
          placeholder={'Ey! Lass was machen'}
          value={task}
          onChangeText={(text) => setTask(text)}
        />
        <TouchableOpacity
          onPress={() => {
            handleAddTask();
          }}
        >
          <View style={styles.addWrapper}>
            <Text style={styles.addText}>+</Text>
          </View>
        </TouchableOpacity>
      </KeyboardAvoidingView>
    </SafeAreaView>
  );
}

Solution

  • Looking at the code, it's first saving and then it's updating the array, so you will always be one step behind on your storage:

    const handleAddTask = () => {
        storeData(); // here you are handling the save
        Keyboard.dismiss();
        setTaskItems([...taskItems, task]); // but you update the values only here
        setTask(null);
      };
    

    To keep your code simple I would suggest you to save each time you have an update on your taskItems but keep in mind you don't need to update the first time you load from storage, so something like this should work:

    export default function App() {
      const [task, setTask] = useState();
      const [taskItems, setTaskItems] = useState([]);
      const [loading, setLoading] = useState(true);
    
      const getData = async () => {
        try {
          const jsonValue = await AsyncStorage.getItem('TASKS');
          const jsonValue2 = JSON.parse(jsonValue);
          if (jsonValue2 !== null) {
            setTaskItems(jsonValue2);
          }
        } catch (e) {
          alert(e);
        } finally {
          setLoading(false)
        }
      };
    
      const storeData = async () => {
        // commenting this line as you don't need to clean the storage each time you write something on it, as you'll just override the existing key 'TASKS'
        // await AsyncStorage.clear();
        
        // here you validate if the data was loaded already before starting watching for changes on the list
        if (!loading) {
          try {
            const jsonValue = await AsyncStorage.setItem(
              'TASKS',
              JSON.stringify(taskItems)
            );
            return jsonValue;
            Reactotron.log(jsonValue);
          } catch (e) {
            alert(e);
          }
        }
      }
    
      useEffect(() => {
        getData();
      }, []);
    
      useEffect(() => {
        storeData()
      }, [taskItems])
    
      const handleAddTask = () => {
        // won't need this line anymore as now everything you update your list it will be saved.
        // storeData();
        Keyboard.dismiss();
        setTaskItems([...taskItems, task]);
        setTask(null);
      };
    
      const completeTask = (index) => {
        let itemsCopy = [...taskItems];
        itemsCopy.splice(index, 1);
        // this will trigger a save as well
        setTaskItems(itemsCopy);
      };
    
      const bearyDustLogo = require('./assets/bearydust-logo-bear-with-text.png');
    

    This way you will always save any updates on the task list and will prevent to save then as empty when first rendering your component.

    Success on your project.