Repository: expo/expo
File: packages/expo-router/src/hooks.ts (TypeScript)

Code before:
  // The second invocation happens after the client has hydrated on initial load, so we look up the data injected
  // by `<PreloadedDataScript />` using `globalThis.__EXPO_ROUTER_LOADER_DATA__`
  if (typeof window !== 'undefined' && globalThis.__EXPO_ROUTER_LOADER_DATA__) {
    if (globalThis.__EXPO_ROUTER_LOADER_DATA__[resolvedPath]) {
      return globalThis.__EXPO_ROUTER_LOADER_DATA__[resolvedPath];
    }
  }

  // Check error cache first to prevent infinite retry loops when a loader fails.
  // We throw the cached error instead of starting a new fetch.
  if (loaderErrorCache.has(resolvedPath)) {
    // eslint-disable-next-line no-throw-literal
    throw loaderErrorCache.get(resolvedPath) as Error;
  }

  // Check cache for route data
  if (loaderDataCache.has(resolvedPath)) {
    return loaderDataCache.get(resolvedPath);
  }

  // Fetch data if not cached
  if (!loaderPromiseCache.has(resolvedPath)) {
    const promise = fetchLoaderModule(resolvedPath)
      .then((data) => {
        loaderDataCache.set(resolvedPath, data);
        loaderErrorCache.delete(resolvedPath); // Clear any previous error
        loaderPromiseCache.delete(resolvedPath);
        return data;
      })
      .catch((error) => {
        const wrappedError = new Error(`Failed to load loader data for route: ${resolvedPath}`, {
          cause: error,
        });
        loaderErrorCache.set(resolvedPath, wrappedError); // Cache the error
        loaderPromiseCache.delete(resolvedPath);
        throw wrappedError;
      });

    loaderPromiseCache.set(resolvedPath, promise);
  }

  return use(loaderPromiseCache.get(resolvedPath)!);
}


Review comment (performance):
```suggestion
        loaderErrorCache.delete(resolvedPath);
```

nit: only pointing this out because it stands out; the comment here (and below) is redundant. I don't think it's worth merging both maps into one state `interface` or union type, since we'll come back to refactor how this whole hook works soon-ish, but I don't think the absence of an atomic cache entry means we need a comment here to explain `loaderErrorCache`

Code after:

  // The second invocation happens after the client has hydrated on initial load, so we look up the data injected
  // by `<PreloadedDataScript />` using `globalThis.__EXPO_ROUTER_LOADER_DATA__`
  if (typeof window !== 'undefined' && globalThis.__EXPO_ROUTER_LOADER_DATA__) {
    if (globalThis.__EXPO_ROUTER_LOADER_DATA__[resolvedPath]) {
      return globalThis.__EXPO_ROUTER_LOADER_DATA__[resolvedPath];
    }
  }

  // Check error cache first to prevent infinite retry loops when a loader fails.
  // We throw the cached error instead of starting a new fetch.
  if (loaderErrorCache.has(resolvedPath)) {
    throw loaderErrorCache.get(resolvedPath);
  }

  // Check cache for route data
  if (loaderDataCache.has(resolvedPath)) {
    return loaderDataCache.get(resolvedPath);
  }

  // Fetch data if not cached
  if (!loaderPromiseCache.has(resolvedPath)) {
    const promise = fetchLoaderModule(resolvedPath)
      .then((data) => {
        loaderDataCache.set(resolvedPath, data);
        loaderErrorCache.delete(resolvedPath);
        loaderPromiseCache.delete(resolvedPath);
        return data;
      })
      .catch((error) => {
        const wrappedError = new Error(`Failed to load loader data for route: ${resolvedPath}`, {
          cause: error,
        });
        loaderErrorCache.set(resolvedPath, wrappedError);
        loaderPromiseCache.delete(resolvedPath);
        throw wrappedError;
      });

    loaderPromiseCache.set(resolvedPath, promise);
  }

  return use(loaderPromiseCache.get(resolvedPath)!);
}