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

Code before:
  const serverDataLoaderContext = use(ServerDataLoaderContext);

  if (!routeNode) {
    throw new Error('No route node found. This is likely a bug in expo-router.');
  }

  const resolvedPath = `/${resolveHref({ pathname: routeNode?.route, params })}`;

  // First invocation of this hook will happen server-side, so we look up the loaded data from context
  if (serverDataLoaderContext) {
    return serverDataLoaderContext[resolvedPath];
  }

  // 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;
      });


Review comment (performance):
```suggestion
    throw loaderErrorCache.get(resolvedPath);
```

Why do we need the casting and `eslint-disable` here?

Code after:
  const params = useLocalSearchParams();
  const serverDataLoaderContext = use(ServerDataLoaderContext);

  if (!routeNode) {
    throw new Error('No route node found. This is likely a bug in expo-router.');
  }

  const resolvedPath = `/${resolveHref({ pathname: routeNode?.route, params })}`;

  // First invocation of this hook will happen server-side, so we look up the loaded data from context
  if (serverDataLoaderContext) {
    return serverDataLoaderContext[resolvedPath];
  }

  // 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;
      });