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

With React 18RC, onSuccess does not get called when using Suspense #1733

Closed
pkellner opened this issue Dec 24, 2021 · 10 comments
Closed

With React 18RC, onSuccess does not get called when using Suspense #1733

pkellner opened this issue Dec 24, 2021 · 10 comments

Comments

@pkellner
Copy link

The below example is running with the package.json referenced at the bottom of this issue.

The code following works as expected which is not using suspense, but using React 18. That is, YES is shown in the rendered output.

import { Suspense, useEffect, useState } from "react";
import useSWR from "swr";

const fetcher = (url) => fetch(url).then((res) => res.json());

function ShowData() {
  const [onSuccessCalled, setOnSuccessCalled] = useState("NO");
  const { data, error } = useSWR(
    "https://jsonplaceholder.typicode.com/todos/2",
    fetcher,
    {
      suspense: false,
      onSuccess(data) {
        console.log("onSuccess called",data);
        setOnSuccessCalled("YES!");
      },
    }
  );
  return (
    <div>
      {JSON.stringify(data)}
      <br />
      onSuccessCalled: {onSuccessCalled}
    </div>
  );
}

function App() {
  return <ShowData />;
}

export default App;

This code, however, has NO remaining showing that onSuccess was never called.

import { Suspense, useEffect, useState } from "react";
import useSWR from "swr";

const fetcher = (url) => fetch(url).then((res) => res.json());

function ShowData() {
  const [onSuccessCalled, setOnSuccessCalled] = useState("NO");
  const { data, error } = useSWR(
    "https://jsonplaceholder.typicode.com/todos/2",
    fetcher,
    {
      suspense: true,
      onSuccess(data) {
        console.log("onSuccess called",data);
        setOnSuccessCalled("YES With Suspense");
      },
    }
  );
  return (
    <div>
      {JSON.stringify(data)}
      <br />
      onSuccessCalled: {onSuccessCalled}
    </div>
  );
}

function App() {
  return (
    <Suspense fallback={<div>Loading....</div>}>
      <ShowData />
    </Suspense>
  );
}

export default App;

package.json follows:

{
  "name": "my-app",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "axios": "^0.24.0",
    "memory-cache": "^0.2.0",
    "next": "12.0.7",
    "react": "^18.0.0-rc.0",
    "react-dom": "^18.0.0-rc.0",
    "swr": "^1.1.1"
  },
  "devDependencies": {
    "@types/node": "16.11.11",
    "@types/react": "17.0.37",
    "eslint": "^7.24.0",
    "eslint-config-next": "^12.0.7",
    "typescript": "4.5.2"
  }
}

@pkellner
Copy link
Author

@promer94 I get the same error with your codesandbox example in both react 17 and 18 (as you have Suspense set to true).

I do notice that if I click on the browser window a bunch that the console output does appear. I'm assuming that has something to do with onfocus events in SWR but that's not really relevant to my issue, which is onsuccess should be called immediately on completion of the promise.

Here attached is a video that shows on success not being called in codesandbox. I tried it in both chrome and safari with the same results.

react-18-not-called-on-success-CRA.mp4

@promer94
Copy link
Collaborator

After reviewing the code. It might be hard to change the current behaviour.

In non-suspense mode

  • hook mounted
  • fire request
  • request resolve
  • call onSuccess callback

But in suspense mode

  • hook try to mount
  • beacause there is no data in cache
  • hook throw a promise and suspend
  • onSuccess will not be called (since hook is not mounted)

Beacuse we want to help user to avioid the problems like #286 #787, the config callback will only be called if the hook is mounted.

Could you share your real world use cases with us ?

@pkellner
Copy link
Author

Example Repo I will use in my course with non-working onSuccess:
https://github.com/pkellner/pluralsight-react-18-suspense-swr-problem
(This code is completely self contained and can be run with npm i -f and npm run dev

onSuccess code here:
https://github.com/pkellner/pluralsight-react-18-suspense-swr-problem/blob/master/src/components/CityListItems.js

My real world problem is that I have a list of cities that I load inside a suspense boundary and after the list is loaded, I want to select the first item in the list returned so that other components on the page can react and render.

I'm doing this for a Pluralsight Course that includes Suspense so it's important that I provide a best practices example of how to do this. I was hoping I could do it directly with suspense, but I have no idea how to capture the completion of the returned promise that is inside my suspense boundary. I've tried useEffect, but without success. I was hoping that the return of what gets passed into startTransition would get executed at completion but that does not work either.

@pkellner
Copy link
Author

Also, is there a way I can set SWR to always make a fresh request and never use the cache? That would be more desirable for teaching Suspense.

I've tried setting all the option as aggressively as I could but the cache still happens.

I'm on my mobile right now so can't provide options I set trying to disable caching.

@pkellner
Copy link
Author

@promer94 , I've been trying to solve this on my own. I'm still not seeing how I can work with useEffect to make this work. My last try was to try and monkey with the fetcher, but sometimes, I get an error saying the completion happens after the component is unmounted (which kind of makes sense to me).

Let me know if you need a better simpler example then then my full teaching app. Below is my attempt that works some of the time, but clearly not the final answer.

import { Suspense, useState } from "react";
import useSWR from "swr";

const fetchWithCallback = (url, completionCallback) =>
  new Promise((resolve) => {
    setTimeout(() => {
      const data = <div>Return from Promise</div>;
      resolve(<div>Return from Promise</div>);
      if (completionCallback) {
        completionCallback(data);
      }
    }, 3000);
  });

function ShowData() {
  const [onSuccessCalled, setOnSuccessCalled] = useState("NO");
  const [onFetcherCompletionCalled, setOnFetcherCompletionCalled] =
    useState("NO");
  const [fetchCompletionData, setFetcherCompletionData] = useState();

  const { data } = useSWR(
    "/api/suspense",
    () =>
      fetchWithCallback(`/api/suspense`, (retData) => {
        console.log("retData", retData);
        setOnFetcherCompletionCalled("YES");
        setFetcherCompletionData(retData);
      }),
    {
      suspense: true,
      onSuccess(data) {
        console.log("onSuccess called", data);
        setOnSuccessCalled("YES With Suspense");
      },
    }
  );

  return (
    <div>
      {data}
      <br />
      onSuccessCalled: {onSuccessCalled}<br/>
      onFetcherCompletionCalled: {onFetcherCompletionCalled}<br/>
      fetchCompletionData: {fetchCompletionData}<br/>
    </div>
  );
}

function AppFallback() {
  return (
    <div>
      <div>FALLB</div>
    </div>
  );
}

function App() {
  return (
    <Suspense fallback={<AppFallback />}>
      <ShowData />
    </Suspense>
  );
}

export default App;

@promer94
Copy link
Collaborator

promer94 commented Dec 29, 2021

pkellner/pluralsight-react-18-suspense-swr-problem#1
Hope this works for you

@pkellner
Copy link
Author

Thanks for trying @promer94 but I'm still not getting the behavior I'm wanting. That is, the first city being selected after the citylist downloads. Take a look at the "React 17" version at the URL to see what I mean. That is the one that has the onSuccess working. I'm trying to do the same thing with React 18 and suspense true.

https://pluralsight-react-18-first-look.peterkellner.net/

@pkellner
Copy link
Author

pkellner commented Dec 29, 2021

Thanks @promer94 Your second pull request did achieve my goal. I hope there is a clean long term solution for using useSwr and suspense. It does feel like it's a step backward to move to suspense to achieve what was a lot easier before suspense was added.

Also, I add the startTransition to the useEffect so that the pending functionality works after the first row is highlighted and the middle column of data loads.

Thanks for sticking with me on this.

useEffect(() => {
    if (data && data?.length > 0) {
      setTempId(data[0].id);
      startTransition(() => {
        setSelectedCityId(data[0].id);
        setSelectedCityName(data[0].city);
        setSelectedStateName(data[0].state);
      });
    }
  }, [data, setSelectedCityId, setSelectedCityName, setSelectedStateName]);

pkellner/pluralsight-react-18-suspense-swr-problem#2

@shuding
Copy link
Member

shuding commented Dec 30, 2021

Thanks for all the feedback! As you know that currently suspense is still experimental and we’re still exploring the best practices with SWR for the various use cases.

@pkellner pkellner reopened this Apr 7, 2022
@pkellner pkellner closed this as completed Apr 8, 2022
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

3 participants