Async-storage: Revisiting Hooks implementation

Created on 5 Mar 2019  路  14Comments  路  Source: react-native-async-storage/async-storage

You want to:

Discuss required changes to current 'hook-ish' implementation for Hooks support.

Details:

As you can see here, we've got a nice wrapper on top AsyncStorage, to mimic hooks usage.
This discussion here is to come up with better and valid implementation, that leverages hooks API.

Example implementation could look like this:

function useAsyncStorage(key, defaultValue) {
  const [storageValue, updateStorageValue] = useState(defaultValue);

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

  async function getStorageValue() {
    let value = defaultValue;
    try {
      value = (await AsyncStorage.getItem(key)) || defaultValue;
    } catch (e) {
      // handle here
    } finally {
      updateStorageValue(value);
    }
  }

  async function updateStorage(newValue) {
    try {
      await AsyncStorage.setItem(key, newValue);
    } catch (e) {
      // handle here
    } finally {
      getStorageValue();
    }
  }

  return [storageValue, updateStorage];
}

The problem in here is that, once we got a value inside AsyncStorage, we'll be getting two rerenders one component mounting - one returning the default value, second the actual stored data.

I'm more than happy to discuss this, so please let me know what you think.

enhancement question

Most helpful comment

const useAsyncStorage = (key, defaultValue) => {
  const [storageValue, updateStorageValue] = useState(defaultValue);
  const [updated, setUpdated] = useState(false);

  async function getStorageValue() {
    let value = defaultValue;
    try {
      value = JSON.parse(await AsyncStorage.getItem(key)) || defaultValue;
    } catch (e) {
    } finally {
      updateStorageValue(value);
      setUpdated(true);
    }
  }

  async function updateStorage(newValue) {
    try {
      if (newValue === null) {
        await AsyncStorage.removeItem(key);
      } else {
        const value = JSON.stringify(newValue);
        await AsyncStorage.setItem(key, value);
      }
    } catch (e) {
    } finally {
      setUpdated(false);
      getStorageValue();
    }
  }

  useEffect(() => {
    getStorageValue();
  }, [updated]);

  return [storageValue, updateStorage];
};

All 14 comments

For now, I do it like this:

import React, { useState, useEffect } from "react";
import { View, TextInput } from "react-native";
import AsyncStorage from "@react-native-community/async-storage";

export default function MyApp(props) {
  const name = inputPersist("@name");

  return (
    <View>
      <TextInput {...name}/>
    <View>
  );
}

function inputPersist(key) {
  const [value, setValue] = useState();

  function readItem() {
    AsyncStorage.getItem(key).then(itemValue => setValue(itemValue));
  }

  useEffect(readItem, []);

  function handleChangeText(input) {
    AsyncStorage.setItem(key, input);
    setValue(input);
  }

  return {
    value,
    onChangeText: handleChangeText
  };
}

The value of TextInput is now persist.

Maybe using a value to keep track of the status (updated or not) of the value would help?

  useEffect(() => {
    getStorageValue();
  }, [updated]);
const useAsyncStorage = (key, defaultValue) => {
  const [storageValue, updateStorageValue] = useState(defaultValue);
  const [updated, setUpdated] = useState(false);

  async function getStorageValue() {
    let value = defaultValue;
    try {
      value = JSON.parse(await AsyncStorage.getItem(key)) || defaultValue;
    } catch (e) {
    } finally {
      updateStorageValue(value);
      setUpdated(true);
    }
  }

  async function updateStorage(newValue) {
    try {
      if (newValue === null) {
        await AsyncStorage.removeItem(key);
      } else {
        const value = JSON.stringify(newValue);
        await AsyncStorage.setItem(key, value);
      }
    } catch (e) {
    } finally {
      setUpdated(false);
      getStorageValue();
    }
  }

  useEffect(() => {
    getStorageValue();
  }, [updated]);

  return [storageValue, updateStorage];
};
const useAsyncStorage = (key, defaultValue) => {
  const [storageValue, updateStorageValue] = useState(defaultValue);
  const [updated, setUpdated] = useState(false);

  async function getStorageValue() {
    let value = defaultValue;
    try {
      value = JSON.parse(await AsyncStorage.getItem(key)) || defaultValue;
    } catch (e) {
    } finally {
      updateStorageValue(value);
      setUpdated(true);
    }
  }

  async function updateStorage(newValue) {
    try {
      if (newValue === null) {
        await AsyncStorage.removeItem(key);
      } else {
        const value = JSON.stringify(newValue);
        await AsyncStorage.setItem(key, value);
      }
    } catch (e) {
    } finally {
      setUpdated(false);
      getStorageValue();
    }
  }

  useEffect(() => {
    getStorageValue();
  }, [updated]);

  return [storageValue, updateStorage];
};

It still returns the default value in the first time. Can we get the actual stored data in the first time?

We're going to revisit it in v2.

I iterated on the previous version @edwinvrgs proposed and came up with this (in Typescript):

import AsyncStorage from '@react-native-community/async-storage'
import { useEffect, useState } from 'react'

const useAsyncStorage = <T>(key: string, defaultValue: T): [T, (newValue: T) => void, boolean] => {
  const [state, setState] = useState({
    hydrated: false,
    storageValue: defaultValue
  })
  const { hydrated, storageValue } = state

  async function pullFromStorage() {
    const fromStorage = await AsyncStorage.getItem(key)
    let value = defaultValue
    if (fromStorage) {
      value = JSON.parse(fromStorage)
    }
    setState({ hydrated: true, storageValue: value });
  }

  async function updateStorage(newValue: T) {
    setState({ hydrated: true, storageValue: newValue })
    const stringifiedValue = JSON.stringify(newValue);
    await AsyncStorage.setItem(key, stringifiedValue);
  }

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

  return [storageValue, updateStorage, hydrated];
};

export default useAsyncStorage

A couple notable tweaks:

  • Rename some things to use the terminology "hydrate" instead of "update"
  • Update the entire state object at once (rather than two separate steps), to reduce the number of renders
  • return hydrate so it can be used conditionally. This is a sort of workaround for the issue @tranhiepqna mentioned. AsyncStorage is (by definition) asynchronous, so I don't think there's any way we're going to be able to return the value on the first time through here. Instead, the idea is return the state that our hook is in, so people can do something like if (hydrated) { doTheThing() }

Note: I took out the error-handling for now, because I don't need it for my use-case

Man @dkniffin, great approach. I came up with a similar solution in my current project but nothing as polish as this proposal of you. The only thing that I can say is that maybe we can use useReducer now that we have 2 values in the state, and with that maybe we can include an error value to the state.

@edwinvrgs Yep, agreed. I think that makes sense. I've been working with this a bit more and tweaking it as a I go. The two notable changes I've made so far are:

  1. rename hydrated to synced
  2. Change updateStorage's setState to synced: false instead of true. That way, we can return whether the variable we've returned matches what's in storage. I could see a use-case where hydrated is also useful, just to know whether we've fetched from storage yet at the beginning, but I think synced is more useful, at least for what I'm doing.

My team has been using this solution I wrote on an app we're working on. We've been using it for a while now, and there's one gotcha we've just run into: updateStorage will immediately write into localStorage. In our case, we hooked up an input box to updateStorage, and the result was that every time someone typed into the box, it would write to storage (ie disk write), which caused some pretty bad lag behavior.

We fixed that by having the form state stored separately in memory and only write to it at specific times (when the user clicks a "Next" button). I could see that solution being incorporated into the snippet I wrote above (which ideally I'm hoping gets integrated into this library at some point), so that this useAsyncStorage keeps an in-memory state, which can be updated with a setState-like function, then only syncs to localStorage when updateState is called. Or, that functionality could be designated as "outside the scope of this solution", with a warning in the docs.

On this topic, @Krizzu is this hook revisit still in the works for v2? When should we expect that?

Reopening for further discussions to improve hooks for v1.

@dkniffin I know it's been a while, but your solution looks pretty solid. What about *Many or removing functionality? I guess those methods could have their own hooks like useRemove / useAsyncStorageMany ? WDYT?

@Krizzu I'm not sure what you mean by What about *Many or removing functionality?. Looks like you might have made a typo?

As for this solution, you are welcome to use it. However, I am no longer working on the project that I implemented that in, so I can no longer attest to whether it's working or not, unfortunately, and I'm not actually working on any RN projects at the moment. Sorry.

Can't useXXX functions be async in the first place?

[value, setValue] = await useAsyncStorage("@key") maybe together with suspend?

On the other hand, maybe such values should be loaded outside the component that needs them in the first place? Then the async nature of the refresh when the value is actually retrieved is not that obvious?

@pke No, since the render() function cannot be async. You have to have a default value (or undefined if you can't provide one), which is a placeholder until the actual value from the AsyncStorage is read.

This means, that there will always be at least one unavoidable re-render.

@dkniffin @Krizzu to not always write to disk everytime the state changes, we could also use some sort of debouncing/throttling. I've wrote an example useDebouncedEffect hook here, which can be used to only write to disk (save to async storage) after xms of no state updates.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Waqas-Jani picture Waqas-Jani  路  28Comments

cohawk picture cohawk  路  28Comments

burhanahmed92 picture burhanahmed92  路  27Comments

mxmzb picture mxmzb  路  19Comments

cpojer picture cpojer  路  34Comments