Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fetcher isn't run again when error boundary resets using Suspense. #2935

Open
ConsoleTVs opened this issue Apr 10, 2024 · 1 comment
Open

Comments

@ConsoleTVs
Copy link

Bug report

Description / Observed Behavior

The fetcher promise isn't re-executed when the error boundary resets
and the component is run again. Happens when Suspense is enabled.

Expected Behavior

The console.log should be fired again when the retry button is clicked.

Repro Steps / Code Example

import { Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";
import useSWR, { SWRConfig, SWRConfiguration } from "swr";

async function fetcher(key: string) {
  console.log("fetching", key);
  const response = await fetch(key);

  if (!response.ok) {
    throw new Error("Failed to fetch");
  }

  return await response.json();
}

function User() {
  const { data } = useSWR("https://jsonplaceholder.typicode.com/users/1222");

  return <div>{data?.name}</div>;
}

const config: SWRConfiguration = {
  fetcher,
  suspense: true,
  revalidateOnMount: true,
};

function App() {
  return (
    <SWRConfig value={config}>
      <ErrorBoundary
        fallbackRender={({ error, resetErrorBoundary }) => (
          <>
            <div>{error.toString()}</div>
            <button onClick={resetErrorBoundary}>Retry</button>
          </>
        )}
      >
        <Suspense fallback={<div>Loading</div>}>
          <User />
        </Suspense>
      </ErrorBoundary>
    </SWRConfig>
  );
}

export default App;

Additional Context

{
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-error-boundary": "^4.0.13",
    "swr": "^2.2.5"
  },
  "devDependencies": {
    "@types/react": "^18.2.66",
    "@types/react-dom": "^18.2.22",
    "@typescript-eslint/eslint-plugin": "^7.2.0",
    "@typescript-eslint/parser": "^7.2.0",
    "@vitejs/plugin-react": "^4.2.1",
    "eslint": "^8.57.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.6",
    "typescript": "^5.2.2",
    "vite": "^5.2.0"
  }
}
@ConsoleTVs
Copy link
Author

ConsoleTVs commented Apr 11, 2024

This likely happens due promise catching. The only way I've found to invalidate the cache is to change the key of the SWR hook. The example below uses an array as the key but you may also use other data structures, or even the same url with different query params. In the end, you have to invalidate the last key. Think of it as a useMemo(promise, [key]).

The following solution refetches the exact same url by using an array instead of a different url string:

import {
  PropsWithChildren,
  Suspense,
  createContext,
  useContext,
  useState,
} from "react";
import { ErrorBoundary, FallbackProps } from "react-error-boundary";
import useSWR, { SWRConfig, SWRConfiguration } from "swr";

async function fetcher(key: string | [string, string]) {
  const url = Array.isArray(key) ? key[1] : key;

  console.log("fetching", key, url);
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error("Failed to fetch");
  }

  return await response.json();
}

function User() {
  const key = useContext(KeyContext);
  const { data } = useSWR(() => [
    key,
    `https://jsonplaceholder.typicode.com/users/1222`,
  ]);

  return <div>{data?.name}</div>;
}

const config: SWRConfiguration = {
  fetcher,
  suspense: true,
};

const KeyContext = createContext<string>("");

function SafeSuspense({ children }: PropsWithChildren) {
  function random(): string {
    return Math.random().toString(36).slice(2, 7);
  }

  const [key, setKey] = useState(random());

  function reset({ error, resetErrorBoundary }: FallbackProps) {
    function resetBoundary() {
      setKey(random());
      resetErrorBoundary();
    }

    return (
      <>
        <div>{error.toString()}</div>
        <button onClick={resetBoundary}>Retry</button>
      </>
    );
  }

  return (
    <ErrorBoundary fallbackRender={reset}>
      <Suspense fallback={<div>Loading</div>}>
        <KeyContext.Provider value={key}>{children}</KeyContext.Provider>
      </Suspense>
    </ErrorBoundary>
  );
}

function App() {
  return (
    <SWRConfig value={config}>
      <SafeSuspense>
        <User />
      </SafeSuspense>
    </SWRConfig>
  );
}

export default App;

What can we do?

I would advise to have this functionality internally in SWR. I would advice to have a key/id in the memo dependency list that came from a context. That way, you could expose a invalidate() or invalidate(key) so that people could use it in their error boundaries. I am not sure if this solution suits the internals of SWR but for sure it does the trick here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant