Why does AsyncStorage.getItem throw Cannot read property ‘getItem’ of undefined in a React Native app when used via a shared npm package?

I’m building a modular system consisting of multiple npm packages for React and React Native projects. Here’s the setup:

  1. replyke-core:

    • Contains shared logic for React and React Native.
    • Includes a conditional getAsyncStorage function to dynamically import @react-native-async-storage/async-storage only when used in React Native.
  2. replyke-rn:

    • A React Native-specific package that consumes replyke-core.
  3. Final consuming project:

    • A React Native app consuming replyke-rn.

Problem

In the final consuming project, calling AsyncStorage.getItem directly works without issue, but when I use it indirectly through replyke-core, it throws the following error:

(NOBRIDGE) ERROR Cannot read property 'getItem' of undefined


Implementation

In replyke-core, I have the following refreshToken function in my authentication context:

const refreshToken = useCallback(async () => {
  try {
    const path = `/auth/refresh`;

    let refreshToken = null;

    // Use helper function to dynamically load AsyncStorage
    if (isReactNative()) {
      const AsyncStorage = await getAsyncStorage();
      console.log("Using AsyncStorage:", AsyncStorage);

      if (!AsyncStorage) {
        console.error("Failed to retrieve AsyncStorage, skipping refresh.");
        return;
      }

      console.log("Checking getItem type:", typeof AsyncStorage.getItem);
      if (typeof AsyncStorage.getItem !== "function") {
        throw new Error("AsyncStorage.getItem is not a function.");
      }

      refreshToken = await AsyncStorage.getItem("refreshToken");
      console.log("Retrieved refreshToken:", refreshToken);

      if (!refreshToken) {
        console.log("No refresh token found.");
        return;
      }
    }

    const response = await axios.post(
      path,
      isReactNative()
        ? { projectId, refreshToken } 
        : { projectId },
      { withCredentials: !isReactNative() }
    );

    const { accessToken: newAccessToken, user: newUser } = response.data;
    setAccessToken(newAccessToken);
    setUser(newUser);
    return newAccessToken;
  } catch (err) {
    handleError(err, "Refresh error: ");
  }
}, [projectId]);

useEffect(() => {
  const fetchInitial = async () => {
    await refreshToken();
    setLoadingInitial(false);
  };
  fetchInitial();
}, [projectId]);

This function uses a helper function to dynamically load AsyncStorage:

export async function getAsyncStorage() {
  try {
    const module = await import("@react-native-async-storage/async-storage");

    const AsyncStorage = module.default;

    if (!AsyncStorage) {
      throw new Error("AsyncStorage is not defined.");
    }
    if (typeof AsyncStorage.getItem !== "function") {
      throw new Error("AsyncStorage.getItem is not a function.");
    }

    return AsyncStorage;
  } catch (err) {
    console.error("Error loading AsyncStorage:", err);
    return null;
  }
}

The @react-native-async-storage/async-storage package is declared as a peerDependency in both replyke-core and replyke-rn:

"peerDependencies": { "@react-native-async-storage/async-storage": "1.x" }

And it is installed in the final consuming project:

npm install @react-native-async-storage/[email protected]


Observations

  1. Direct Usage in the Final Project Works: When I test AsyncStorage directly in the final consuming project, it works as expected:
useEffect(() => {
      const test = async () => {
        const value = await AsyncStorage.getItem("my-key");
        console.log({ value }); // Logs: { value: null }
      };
      test();
    }, []);
  1. Dynamic Import Logs: When debugging the logs show that AsyncStorage is resolved correctly and getItem is a function (identical when using dynamic or static imports):
(NOBRIDGE) LOG  Using AsyncStorage: { clear: [Function clear], getItem: [Function getItem], ... }
(NOBRIDGE) LOG  Checking getItem type: function

But the call to getItem still fails:

(NOBRIDGE) ERROR Cannot read property 'getItem' of undefined

  1. Static Import Fails Too: To rule out dynamic imports as the issue, I replaced getAsyncStorage with a static import in replyke-core:
import AsyncStorage from "@react-native-async-storage/async-storage";
    
refreshToken = await AsyncStorage.getItem("refreshToken");

The same error occurs.

  1. When I Remove the refreshToken Function Call: Removing the useEffect that calls refreshToken stops the error.

What I’ve Tried

  1. Used .then/.catch Instead of await:
    I refactored the refreshToken function to use a .then/.catch chain for handling the getAsyncStorage promise, as it could potentially affect scoping or execution context. The AsyncStorage object resolved correctly, and getItem was still confirmed to be a function, but the same error (Cannot read property ‘getItem’ of undefined) occurred when calling getItem.
  2. Declaring @react-native-async-storage/async-storage as a peerDependency in both replyke-core and replyke-rn.
  3. Verifying that the final consuming project has the dependency installed (1.23.1).
  4. Using both dynamic and static imports in replyke-core.
  5. Running npm dedupe to ensure no duplicate versions of @react-native-async-storage/async-storage exist.
  6. Adding extensive logging to confirm that AsyncStorage and its methods are resolved correctly.

Environment

  • React Native: 0.76.2
  • Expo: 52.0.7
  • AsyncStorage: 1.23.1

Question

Why does AsyncStorage.getItem throw Cannot read property 'getItem' of undefined when used through the replyke-core package, but works correctly when used directly in the consuming project? How can I resolve this issue?