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

How to use React Suspense to wait for content to load #469

Closed
hemm1 opened this issue Mar 3, 2022 · 5 comments
Closed

How to use React Suspense to wait for content to load #469

hemm1 opened this issue Mar 3, 2022 · 5 comments

Comments

@hemm1
Copy link

hemm1 commented Mar 3, 2022

Thank you for the great library! In our application we use a callback to wait for content to load before printing it, and it works fine. We are, however, also using Suspense for some of our data fetching, and I am curious as to how we would use Suspense and this library together. It was mentioned in another issue that it can be done (#440 (comment)), but when searching for it, I couldn't find any info on how to actually do it. Does anyone have any pointers on how we would use Suspense to elegantly wait for a child component to fetch all its data before printing it?

@MatthewHerbst
Copy link
Owner

MatthewHerbst commented Mar 3, 2022

Hello, glad you're enjoying the library, thank you! I would highly suggest staying away from Suspense right not for a production website unless you can't find another way of solving your problem. The Suspense API is very experimental, and is likely to see massive breaking changing in React 18.

Also, you say that you are "using Suspense for some of our data fetching," but Suspense does not perform data fetching. Are you using Relay?

With that being said, my comment in the other issue was saying that you could prevent rendering of the entire react-to-print stack until all your components had loaded. So for example, something like:

const response = fetchData();
return (
  <Suspense>
    <PrintingView />
  </Suspense>
  <NormalView />
)

That's a very contrived example, and I've never myself actually used Suspense with react-to-print, but that's how I imagined it would look like.

@hemm1
Copy link
Author

hemm1 commented Mar 4, 2022

Thank you for your reply! I was a bit unclear in my question, sorry about that. We are using fetch for our actual data fetching, but we are wrapping it all in recoiljs, which is designed to work with Suspense for showing a fallback UI while the data is actually being fetched. We have been using this approach for several months across several apps in production and we are very happy with it. I might, of course, eat my words if we have to rewrite all our apps in order to update to React 18, but we are "only" using the <React.Suspense> component, and not any of the @experimental concurrent mode stuff. Also, we are quite comfortable with experimenting a bit with new APIs :)

I tried your example this morning, and I couldn't get it to work. Quick code example:

  const componentRef = useRef(null);
  const [isPrinting, setIsPrinting] = useState(false);

  const handlePrint = useReactToPrint({
    content: () => componentRef.current,
    onBeforeGetContent: () => setIsPrinting(true),
    onAfterPrint: () => setIsPrinting(false),
  });

    return (
      <button onClick={handlePrint}>
        {isPrinting && (
          <div style={{ display: 'none' }}>
            <React.Suspense fallback={null}>
              <PrintablePriceBoard id={id} ref={componentRef} />
            </React.Suspense>
          </div>
        )}
        {children}
      </button>
    );

The PrintablePriceBoard-component fetches its own data via recoiljs hooks, but it doesn't seem that the printing actually waits for the data to fetch before it tries to print. When i click the link the first time I immediately get the error "There is nothing to print because the \"content\" prop returned \"null\". Please ensure \"content\" is renderable before allowing \"react-to-print\" to be called.". When I click the link the second time, however, the data has already been fetched and cached in the component and the printing works like a charm.

As I mentioned, we have a working implementation using callbacks and avoiding React Suspense, so it is no must for us to use Suspense for this, but in case anyone else is having issues with this, I thought it was worth asking about :)

I'd be happy to test anything else if you have any ideas on how I could make it work, but for now I think I'll just stick with our callback solution and move forwards. Thank you again! 😄

@MatthewHerbst
Copy link
Owner

MatthewHerbst commented Mar 4, 2022

Thanks for the clarifications!

So, what you need is for onBeforeGetContent to take a Promise that isn't resolved until PrintablePriceBoard has rendered. Here is an example of what I think should work, but I haven't tested it:

  const componentRef = useRef(null);
  const [isPrinting, setIsPrinting] = useState(false);

  const getContentRef = useRef(null);

  const handlePrint = useReactToPrint({
    content: () => componentRef.current,
    onBeforeGetContent: () => {
      return new Promise((resolve) => {
        getContentRef.current = resolve;
        setIsPrinting(true);
      });
    },
    onAfterPrint: () => setIsPrinting(false),
  });

  useEffect(() => {
    // I don't actually know if checking the componentRef.current will work here the
    // way I think it will, but I feel like this check should be good enough
    if (isPrinting && getContentRef.current && componentRef.current) {
      getContentRef.current(); // Resolves the promise passed to `onBeforeGetContent`
    }
  }, [isPrinting, getContentRef.current, componentRef.current]);

  return (
    <button onClick={handlePrint}>
      {isPrinting && (
        <div style={{ display: 'none' }}>
          <React.Suspense fallback={null}>
            <PrintablePriceBoard id={id} ref={componentRef} />
          </React.Suspense>
        </div>
      )}
      {children}
    </button>
  );

@hemm1
Copy link
Author

hemm1 commented Mar 7, 2022

Hi again! I have been digging a little into this this morning, and I have learned something new and have gotten this to work for us in a good way (in my opinion, at least). Your example did not work out of the box, and it seems the problem was not really with Suspense, it was with the use of refs. It seems that one shouldn't rely on ref.current in a useEffect dependency array. One has no guarantee that React actually notifies when ref.current changes. This is by design, and explained a bit by one of the React core dev team here: facebook/react#14387 (comment)

The comment links to an explanation of using useCallback instead of useRef, and that approach (after some tweaking) worked really well. Updated code example below:

const componentRef = useRef(null);
const onBeforeGetContentResolve = useRef<(() => void) | null>(null);
const [isPrinting, setIsPrinting] = useState(false);

const handlePrint = useReactToPrint({
    content: () => componentRef.current,
    onBeforeGetContent: () => {
      return new Promise((resolve) => {
        setIsPrinting(true);
        onBeforeGetContentResolve.current = resolve;
      });
    },
    onAfterPrint: () => setIsPrinting(false),
});

const callbackRef = useCallback((node) => {
    if (node !== null && onBeforeGetContentResolve.current !== null) {
      componentRef.current = node;
      onBeforeGetContentResolve.current();
    }
}, []);

return (
  <button onClick={handlePrint}>
    <div style={{ display: 'none' }}>
      {isPrinting && (
        <React.Suspense fallback={null}>
          <PrintablePriceBoard id={id} ref={callbackRef} />
        </React.Suspense>
      )}
    </div>
    {children}
  </button>
);

The PrintablePriceBoard component now gets its data in a totally self-contained way using recoiljs, suspense and fetch which also makes it really easy to render it wherever I want in the application while developing so that we can tweak the design of it without having to actually click it and see the print preview :)

I would have preferred to not use a second useRef to provide to the content function and just use the actual node itself in some more straightforward way, but I couldn't figure out how without always getting the "prop returned null" error. Anyway, this works fine and I am totally happy with it.

Again, thank you so much for your help and your awesome library!

@MatthewHerbst
Copy link
Owner

Thanks for reporting back! That totally makes sense about refs, and the useCallback solution is really elegant, thanks for sharing. I'm glad you got it working!

I like the idea of content possible taking a Promise and just using the resolved value from that instead of needing a second ref. I probably don't have the time to work on that for a few weeks but I'll add it to my list! Always happy to accept PRs as well 😄

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

2 participants