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 wait until content is loaded? #440

Closed
MarcosCunhaLima opened this issue Dec 8, 2021 · 9 comments
Closed

How to wait until content is loaded? #440

MarcosCunhaLima opened this issue Dec 8, 2021 · 9 comments
Labels

Comments

@MarcosCunhaLima
Copy link

Hi

Is there a way to wait until all content is properly loaded in order to print?

I have a table which has a button in order to print the selected record.

I have a Form component which takes care of fetching some data, etc and that component returns what would be printed. While this component is fetching data, it shows a circular progress.

What is happening is that sometimes (when net is slow), the printing process shows this circular progress and not the finished component.

Thanks a lot for your very useful component.

@MatthewHerbst
Copy link
Owner

MatthewHerbst commented Dec 8, 2021

Hello. You'll want to use the onBeforeGetContent callback and have it return a Promise. This is a little tricky since you're waiting for React state to update as well, and using hooks there is no easy way to know when state is done updating, so you need to use the useEffect hook to check if the state has updated.

If you are using class components instead this becomes much simpler, since you could replace setDataLoaded(true); with this.setState({ dataLoaded: true }, resolve) and not worry about the stuff in useEffect at all.

This is a rough example, please let me know if you run into any trouble with it.

const [dataLoaded,  setDataLoaded] = useState(false);
const onBeforeGetContentResolve = useRef();
const componentRef = useRef();

const handleOnBeforeGetContent = () => {
  return new Promise((resolve) => { // `react-to-print` will wait for this Promise to resolve before continuing
    // Load data
    onBeforeGetContentResolve.current = resolve;
    setDataLoaded(true); // When data is done loading
  });
};

const handlePrint = useReactToPrint({
  content: () => componentRef.current,
  onBeforeGetContent: handleOnBeforeGetContent,
});

useEffect(() => {
  if (dataLoaded) {
    // Resolves the Promise, telling `react-to-print` it is time to gather the content of the page for printing
    onBeforeGetContentResolve.current();
  }
}, [dataLoaded, onBeforeGetContentResolve]);

return (
  <div>
    <button onClick={handlePrint}>Print this out!</button>
    <ComponentToPrint ref={componentRef} />
  </div>
);

@MarcosCunhaLima
Copy link
Author

MarcosCunhaLima commented Dec 10, 2021 via email

@MarcosCunhaLima
Copy link
Author

MarcosCunhaLima commented Dec 14, 2021

Hi Matthew

I thought that the onBeforeGetContent would be called anyway when the component is rendered or it needs to be printed but it doesn't get called:

   const refImpressao = React.useRef()
   const onBeforeGetContentResolve = React.useRef();
   const [loaded, setLoaded] = React.useState(false)

   const handleOnBeforeGetContent = () => {
      return new Promise((resolve) => {
         // Load data
         console.log('set onBeforeContentResolve')
         onBeforeGetContentResolve.current = resolve;
      
      });
   };

   React.useEffect(() => {
      if (loaded) {
         console.log('loaded')
         // as onBefore... is empty, it never runs this code
         onBeforeGetContentResolve.current && onBeforeGetContentResolve.current();
      }
   }, [loaded, onBeforeGetContentResolve]);

   const handleClick = useReactToPrint({
      content: () => refImpressao.current,
      onBeforeGetContent: handleOnBeforeGetContent
   });


   return (
      <>
         <div style={{ display: "none" }}>
            <div ref={refImpressao}>
               <ComponentToPrint  setLoaded={setLoaded} />
            </div>
         </div>

         <Button  onClick={handleClick}>
            Print
         </Button>
      </>

It's hard to follow your example as the fetch logic is in the component being printed so I have to use a state variable to make this component alert when it loaded but the problem is that I don't understand when onBeforeGetContent is called.

@MatthewHerbst
Copy link
Owner

onBeforeGetContent is called right before react-to-print fetches the content from the DOM. It's the last chance to change anything on the page before the print happens. react-to-print will wait to start the printing process until the Promise returned by onBeforeGetContent is resolved, allowing you to do async operations within it, such as setting state and/or fetching data.

The useEffect you have is never called during the onBeforeGetContent run because you aren't changing state within onBeforeGetContent, meaning the component will never re-render, and useEffect is called on every render of the component. That's why in my example I did setDataLoaded(true); inside of the onBeforeGetContent. I don't see you loading any data or changing any state in the onBeforeGetContent.

   const handleOnBeforeGetContent = () => {
      return new Promise((resolve) => {
         // Load data
         // DO THIS! Load some data and/or make a change, and then change state to trigger a component re-render

         console.log('set onBeforeContentResolve')
         onBeforeGetContentResolve.current = resolve;
      });
   };

@MarcosCunhaLima
Copy link
Author

MarcosCunhaLima commented Dec 14, 2021

The useEffect you have is never called during the onBeforeGetContent run because you aren't changing state within onBeforeGetContent, meaning the component will never re-render, and useEffect is called on every render of the component.

Yes, I change the loaded state (I pass a setLoaded to the right component which is loading asynchronously and sets this state when it's finished loading).
It's hard to "load the data" is the event you suggested as this is not the responsibility of this component to load this data there.
That component is used in many other places in the App and it's his responsibility to load itself based on an Id.

Let's start again. I think this pattern is very common, you have a Component which you set a prop (Id) and it fetches the data asynchronously.

const refImpressao = React.useRef()
const handleClick = useReactToPrint({
     content: () => refImpressao.current,
});

  return (
     <>
        <div style={{ display: "none" }}>
           <div ref={refImpressao}>
              <ComponentToPrint  id={props.id}/>
           </div>
        </div>

        <Button  onClick={handleClick}>
           Print
        </Button>
     </>

This loads the data and shows up itself. It is used in lots of other places.
If I run this code, sometimes when I click on the button, the ComponentToPrint is not ready yet and it prints a blank page (or a progress bar - whatever this component shows up when it's not ready).

I've put again in the simplest way just to help you or anybody else in order to come up with a new pattern which I am not seeing currently.

@MatthewHerbst
Copy link
Owner

Is it possible for you to share the code for the ComponentToPrint please, or to possibly even make a working CodeSandbox that shows the issue? I think there might be something with the asynchronous data loading that we'll need to take into account.

@MarcosCunhaLima
Copy link
Author

MarcosCunhaLima commented Dec 16, 2021

Hi Matthew

I've created in codesandBox: ComponentToPrint

It's a very contrived example.
After refresh, it took 3 secs to show "finished". If you click the button to print before it, you see "loading" message.

What I'm looking for is a "simple" way in order to make ReactToPrint wait for a finished component in order to print. Here is the code:

import "./styles.css";
import { useReactToPrint } from "react-to-print";
import { useRef, useState, useEffect } from "react";

const ComponentToPrint = () => {
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);
    return setTimeout(() => {
      setLoading(false);
    }, 3000);
  }, []);

  const message = loading ? "loading" : "finished";

  return <div> {message} </div>;
};

export default function App() {
  const printRef = useRef();

  const handleClick = useReactToPrint({
    content: () => printRef.current
  });

  return (
    <div className="App">
      <h1>Print Test</h1>
      <h2>with react-to-print</h2>
      <div ref={printRef}>
        <ComponentToPrint />
      </div>
      <button onClick={handleClick}> Print It </button>
    </div>
  );
}

@MatthewHerbst
Copy link
Owner

Ah, I see. There's no way for react-to-print to know that the child component hasn't finished loading yet. Three solutions:

  1. Move the data loading to the parent
  2. Add a callback prop on the child so it can tell the parent when it has finished loading
  3. Use React Suspense to prevent the parent from loading until its child has finished loading

@MarcosCunhaLima
Copy link
Author

MarcosCunhaLima commented Dec 17, 2021

Yes, the simplest solution is to add a callback and disable the button. Just to leave it registered:

const ComponentToPrint = ({loading, setLoading}) => {
  useEffect(() => {
    let isCurrent = true;
    setLoading(true);
    const id = setTimeout(() => {
      if (isCurrent) setLoading(false);
    }, 3000);
    return () => {
      isCurrent = false;
      clearTimeout(id);
    };
  }, []);

  const message = loading ? "loading" : "finished";

  return <div> {message} </div>;
};

export default function App() {
  const printRef = useRef();
  
  const [loading, setLoading] = useState(false);

  const handleClick = useReactToPrint({
    content: () => printRef.current
  });

  return (
    <div className="App">
      <h1>Print Test</h1>
      <h2>with react-to-print</h2>
     
        <div ref={printRef}>
          <ComponentToPrint setLoading={setLoading} loading={loading}/>
        </div>
      
      <button onClick={handleClick} disabled={loading}> Print It </button>
    </div>
  );
}

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

No branches or pull requests

2 participants