Skip to content

useAsyncStorage is NOT a HOOK #635

@seancheung

Description

@seancheung

Current behavior

According to hooks.js, useAsyncStorage is a wrapper rather than a hook.

// return a new object at every call
export function useAsyncStorage(key: string): AsyncStorageHook {
  return {
    getItem: (...args) => AsyncStorage.getItem(key, ...args), // assign a new prop at every call
    setItem: (...args) => AsyncStorage.setItem(key, ...args),
    mergeItem: (...args) => AsyncStorage.mergeItem(key, ...args),
    removeItem: (...args) => AsyncStorage.removeItem(key, ...args),
  };
}

It will either trigger unexpected re-rendring:

const MyComponent = () => {
  const wrapper = useAsyncStorage('mykey');

  React.useEffect(() => {
    wrapper.getItem().then(res => {
      //...
    })
  }, [wrapper]); // <------ wrapper changes at every re-rendering
}
const MyComponent = () => {
  const { getItem } = useAsyncStorage('mykey');

  React.useEffect(() => {
    getItem().then(res => {
      //...
    })
  }, [getItem]); // <------ getItem changes at every re-rendering
}

Or eslint hooks warnings:

const MyComponent = () => {
  const wrapper = useAsyncStorage('mykey');

  React.useEffect(() => {
    wrapper.getItem().then(res => {
      //...
    })
  }, []); // <------ react-hooks/exhaustive-deps warning
}
const wrapper = useAsyncStorage('mykey'); // <------ react-hooks/rules-of-hooks warning

const MyComponent = () => {
  React.useEffect(() => {
    wrapper.getItem().then(res => {
      //...
    })
  }, []);
}

A workaround is to disable warnings:

// eslint-disable-next-line react-hooks/rules-of-hooks
const wrapper = useAsyncStorage('mykey');

const MyComponent = () => {
  React.useEffect(() => {
    wrapper.getItem().then(res => {
      //...
    })
  }, []);
}

Expected behavior

  • If useAsyncStorage is not a standard hook, it should be renamed to something like withAsyncStorage rather than starting with use which will be treated as a hook.
export function withAsyncStorage(key: string): AsyncStorageHook {
  return {
    getItem: (...args) => AsyncStorage.getItem(key, ...args),
    setItem: (...args) => AsyncStorage.setItem(key, ...args),
    mergeItem: (...args) => AsyncStorage.mergeItem(key, ...args),
    removeItem: (...args) => AsyncStorage.removeItem(key, ...args),
  };
}
  • If useAsyncStorage is designed to be a standard hook, the returned value should be memo-ed
export function useAsyncStorage(key: string): AsyncStorageHook {
  return React.useMemo(() => ({
    getItem: (...args) => AsyncStorage.getItem(key, ...args),
    setItem: (...args) => AsyncStorage.setItem(key, ...args),
    mergeItem: (...args) => AsyncStorage.mergeItem(key, ...args),
    removeItem: (...args) => AsyncStorage.removeItem(key, ...args),
  }), []);
}
  • This should be documented

Repro steps

Environment

  • Platforms tested:
    • Android
    • iOS
    • macOS
    • Windows
  • AsyncStorage version:
  • Expo version:
  • Environment:
  • Logs/Errors that may be relevant:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions