Skip to content

useAsyncStorage is NOT a HOOK #635

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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions