react-nativereact-hooksasyncstorage

How do I use AsyncStorage with a Context Provider in React Native?


I'm new to React Native and am even newer to Context. So any help would be amazing.

I'm building a React Native app that pulls in outside API and creates objects stored in a 'team' that the user picks from data populated by the API.

I am using Context and a Provider as a wrapper for my app. I'm attempting to use AsyncStorage to persist data locally.

I have tried using AsyncStorage in just about every way possible and am not sure where I am going wrong.

Any help is much appreciated as this is the last thing I need for this app.

Edit: Currently, I can save a Team, but, on navigation back to the screen where it shows teams, it will show nothing.

If I go to the home screen and navigate back to the teams screen I will see my data. This process is the same for each team created.

Also, the data is not persisting even though my state being passed to my reducer is coming from getData() which pulls JSON from AsyncStorage.

Context file:

import uuid from 'react-native-uuid'
import AsyncStorage from '@react-native-async-storage/async-storage';

import createDataContext from './createDataContext';

const teamsReducer = (state, action) => {

  switch (action.type) {
    case 'edit_team' :
      return state.map((team) => {
        return team.id === action.payload.id ? 
        action.payload
        : 
        team
      });
    case 'delete_team':
      return state.filter(team => team.id !== action.payload)
    case 'add_team':
      return [
        ...state, 
        { 
          id: uuid.v4(), 
          name: action.payload.name,
          content: action.payload.content
        }
      ];
    default:
      return state;
  }
};

const addTeam = dispatch => {
  return (name, content) => {
    dispatch({ type: 'add_team', payload: { name, content } });
  };
};

const editTeam = dispatch => {
  return (id, name, content) => {
    dispatch({ type: 'edit_team', payload: { id, name, content } })
  };
};

const deleteTeam = dispatch => {
  return (id) => {
    dispatch({ type: 'delete_team', payload: id })
  };
};

export const { Context, Provider } = createDataContext(
  teamsReducer, 
  { addTeam, deleteTeam, editTeam },
  [{ id: 0, name: 'Build Your Team', content: {} }]
);

createDataContext:

import React, { useCallback, useEffect, useReducer } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';

export default (reducer, actions, initialState) => {
  const Context = React.createContext();

  const Provider = ({ children }) => {
    const [state, dispatch] = useReducer(reducer, initialState);

    // console.log(state);

    const storeData = async (value) => {
      // console.log(value);
      try {
        const jsonValue = JSON.stringify(value)
        await AsyncStorage.setItem('@storage_Key', jsonValue)
      } catch (e) {
        // saving error
      }
    }
    storeData(state)

    const getData = async () => {
      try {
        const jsonValue = await AsyncStorage.getItem('@storage_Key')
        return jsonValue != null ? JSON.parse(jsonValue) : null;
      } catch(e) {
        // error reading value
      }
    }

    const storedState = async() => await getData()
    console.log(storedState());

    const boundActions ={};
    for (let key in actions) {
      boundActions[key] = actions[key](dispatch)
    }

    return <Context.Provider value={{ state: storedState(), ...boundActions }}>{children}</Context.Provider>
  };

  return { Context, Provider };
};

How Teams are created:

const BuildTeamsScreen = (props) => {
  const [searchTerm, setSearchTerm] = useState('');
  const [teamName, setTeamName] = useState('');
  const [buildSearchApi, buildResults] = useBuildResults();
  const [teamMembers, setTeamMembers] = useState([])

  const { state, addTeam } = useContext(TeamsContext);

  const showPokemonCard = (param) => {
    if (param !== '') {
      return <FlatList 
      scrollEnabled={false}
      data={buildResults}
      style={{height: 'auto'}}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => {
        const showAddButton = (el) => {
            return (
              teamMembers.length < 6 ?
                <Pressable onPress={() => setTeamMembers([...teamMembers].concat(item))}>
                  <AddPokemonButton name={item.name.replaceAll('-', ' ')} width={'90%'} height={40} />
                </Pressable>
              :
                null
            )
          }
          return(
            <View style={styles.addMonCard}>
              <Pressable style={{flex: 1}} onPress={() => props.navigation.navigate('Detail Modal', { results: [item] })}>
                <ShowAdvancedSearchResult results={item} />
              </Pressable>
              {showAddButton(item)}
            </View>
          )
        }}
      />
    } else return null;
  }

  const addTeamAndGoBack = useCallback(async (name, content) => {;
    if (teamMembers[0] !== null) {
      await addTeam(name, content)
      return (props.navigation.navigate('Teams Tab Nav', { results: content }))
    } else return (props.navigation.navigate('Teams Tab Nav'))
  }, [])

  const showClear = (el, term) => {
    if(el !== null || term !== '') {
      return (
        <Pressable 
          onPress={async() => {
            await buildSearchApi()
            setSearchTerm('')
          }} 
          style={styles.clear}
        >
          <Ionicons name="ios-close-circle" size={18} color="rgb(175, 175, 175)" />
        </Pressable>
      )
    } else return null
  }

  const createTeamMember = (el) => {
    if (el[0] !== undefined) {
      return el.map((item) => {
        const id = uuid.v4()
        return (
          <View key={id} style={{ flexDirection: 'row', width: '90%', alignSelf: 'center' }}>
            <Pressable onPress={() => props.navigation.navigate('Detail Modal', { results: [item] })}>
              <PokemonSlotCard results={item} />
            </Pressable>
            <Pressable style={{ position: 'absolute', alignSelf: 'center', right: 5, zIndex: 1 }} onPress={() => deleteTeamMember(item)}>
              <Ionicons name="ios-remove-circle-outline" size={16} color="#ff0000" />
            </Pressable>
          </View>
        )
      })
    } else return (
      <View style={{ width: '90%', alignSelf: 'center', marginTop: 12 }}>
        <Text adjustsFontSizeToFit={true} numberOfLines={1} style={styles.nullMessage}>Search for a Pokemon to add it to your team</Text>
      </View>
    )
  }

  const deleteTeamMember = (el) => {
    const arr = [...teamMembers];
    const idx = teamMembers.indexOf(el);
    if (idx !== -1) {
      arr.splice(idx, 1)
      setTeamMembers(arr)
    }
  }

  return (
    <HideKeyboard>
      <ScrollView style={styles.container}>
        <View style={styles.teamNameContainer}>
          <TextInput 
            placeholder={'Team Name'} 
            showSoftInputOnFocus={false}
            autoCapitalize='none'
            autoCorrect={false}
            style={styles.inputStyle} 
            placeholderTextColor='rgb(175, 175, 175)'
            value={teamName}
            onChangeText={(text) => setTeamName(text)}
            clearButtonMode='never'
            keyboardAppearance='dark'
            returnKeyType={'done'}
            allowFontScaling={false}
            maxLength={10}
          />
        </View>
        <View style={styles.searchBarContainer}>
          <BuildTeamSearchBar 
            searchTerm={searchTerm} 
            onSearchTermChange={setSearchTerm} 
            onSearchTermSubmit={() => buildSearchApi(searchTerm.replaceAll(' ', '-').toLowerCase())}
            style={styles.searchBar}
          />
          {showClear(buildResults, searchTerm)}
        </View>
        <View style={{height: 'auto'}}>
          {showPokemonCard(searchTerm)}
        </View>
        <View style={{height: 5 }} />
        <View style={styles.teamInfoContainer}>
          <View style={styles.teamSlotContainer}>
            {createTeamMember(teamMembers)}
          </View>
          <Pressable onPress={() => addTeamAndGoBack(teamName, teamMembers)} >
            <SaveTeamButton height={54} width={'90%'} />
          </Pressable>
        </View>
      </ScrollView>
    </HideKeyboard>
  );
};

...

How Teams are being shown:

const TeamsScreen = (props) => {
  const { state, addTeam, deleteTeam } = useContext(TeamsContext);

  const showTeams = useCallback((el) => {
    console.log(el);
    return <FlatList 
      horizontal={false}
      data={el._W}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => {
        // console.log(item.name);
        if (item.id !== 0) {
          return (
            <Pressable onPress={() => props.navigation.navigate('Team Detail', { id: item.id, results: item.content, name: item.name })}>
              <ViewTeams results={item} id={item.id} height={'auto'} width={'100%'} />
            </Pressable>
          )
        } else return null
      }}
    />
  }, [state])

...

Solution

  • You have to be mindful about what you put inside a functional component body, remember that code will execute every time it renders. I see in your code several parts that can be outside the Provider function body, that's probably why you are not storing the correct data, since the first render you are calling storeData, and at that point in time the state has the initial value, so you'll always have the initialState value in your storage.

    To fix it you need to put an useEffect that executes only when the provider mounts, there you get the data from the storage and hydrate the state.

    And to save the state in every change you could also put a useEffect to execute the storeData function every time the state changes.

    Doing some clean up in your provider and implementing the logic I described, it would look like this

    import React, { useCallback, useEffect, useReducer } from 'react';
    import AsyncStorage from '@react-native-async-storage/async-storage';
    
    const storeData = async (value) => {
      try {
        const jsonValue = JSON.stringify(value)
        await AsyncStorage.setItem('@storage_Key', jsonValue)
      } catch (e) {
        // saving error
      }
    }
    
    const getData = async () => {
      try {
        const jsonValue = await AsyncStorage.getItem('@storage_Key')
        return jsonValue != null ? JSON.parse(jsonValue) : null;
      } catch(e) {
        // error reading value
      }
    }
    
    const Context = React.createContext();
    
    const Provider = ({ children }) => {
      const [state, dispatch] = useReducer(reducer, initialState);
      useEffect(() => {
        async function rehydrate() {
          const storedState = await getData()
          if (storedState) {
            dispatch({ type: "hydrate", payload: storedState });
          }
        }
        
        rehydrate()
      }, []);
    
      useEffect(() => {
        storeData(state);
      }, [state])
    
      const boundActions = {};
      for (let key in actions) {
        boundActions[key] = actions[key](dispatch)
      }
    
      return <Context.Provider value={{ state, ...boundActions }}>{children}</Context.Provider>
    };
    
    
    export default {
      Context,
      Provider
    }
    

    and in your reducer add the hydrate action

      case "hydrate":
        return action.payload
    

    I think this article describes more or less what you try to accomplish https://medium.com/persondrive/persist-data-with-react-hooks-b62ec17e843c