Syncing State with AsyncStorage

Nick Pachulski

March 23, 2024

I have a few providers which initialize state, shove it in a context, and then ensure that the state is synchronized with AsyncStorage so that if the app is quit and restarted, the context’s state is rehydrated from AsyncStorage. A few examples of the types of state I tend to treat this way are API tokens and user IDs.

When I realized that I’d been doing this a lot in the past, and twice in my current project (for an API token and user ID), I maybe got too clever. I pulled out the functionality to sync the state with AsyncStorage into it’s own hook.

const [thing, setThing] = useStateSyncedWithAsyncStorage<T>(asyncStorageKey: string, transformer: (thing: T) => string): [T | null, (thing: T) => Promise<void>]

Until someone lets me know why that’s terribly wrong, I’m pretty happy with the result.

import React from "react"
import AsyncStorage from "@react-native-async-storage/async-storage"

const useStateSyncedWithAsyncStorage = <T>(
  asyncStorageKey: string,
  transformer: (value: string) => T,
): [T | null, (thing: T) => Promise<void>] => {
  const [thing, setThingInState] = React.useState<T | null>(null)

  const setThingInStateAndAsyncStorage = async (thing: T): Promise<void> => {
    setThingInState(thing)
    await AsyncStorage.setItem(asyncStorageKey, (thing || "").toString())
  }

  React.useEffect(() => {
    const setStateValueFromAsyncStorageValueOnMount =
      async (): Promise<void> => {
        const thingInAsyncStorage = await AsyncStorage.getItem(asyncStorageKey)
        setThingInState(transformer(thingInAsyncStorage || ""))
      }
    setStateValueFromAsyncStorageValueOnMount()
  }, [setThingInState, asyncStorageKey, transformer])

  return [thing, setThingInStateAndAsyncStorage]
}

export default useStateSyncedWithAsyncStorage

AsyncStorage can only hold strings, so when we get a value out of AsyncStorage and try to put it in a typed useState value, typescript will complain. That’s where the transformer parameter helps. If we’re storing a number in our useState, then we need a way to convert that thing from a string (when we get it out of AsyncStorage) back to it’s typed useState type. Here’s an example of using this hook to store a number on context and keep it synced with AsyncStorage across app launches.

import React from "react"
import type { Provider as ProviderType } from "types/Provider"
import emptyPromiseReturningFunctionForInitializingContexts from "helpers/emptyPromiseReturningFunctionForInitializingContexts"
import useStateSyncedWithAsyncStorage from "hooks/useStateSyncedWithAsyncStorage"

export interface UserIdContextType {
  userId: number | null
  setUserId: (userId: number) => Promise<void>
}

export const UserIdContext = React.createContext<UserIdContextType>({
  userId: null,
  setUserId: emptyPromiseReturningFunctionForInitializingContexts,
})

const UserIdProvider: ProviderType = ({ children }) => {
  const [userId, setUserId] = useStateSyncedWithAsyncStorage<number>(
    "User ID",
    Number,
  )

  return (
    <UserIdContext.Provider value={{ userId, setUserId }}>
      {children}
    </UserIdContext.Provider>
  )
}

export default UserIdProvider

Maybe I got too clever. For now, I think I like it.