diegomura / react-pdf

📄 Create PDF files using React
https://react-pdf.org
MIT License
14.34k stars 1.13k forks source link

Uncaught Error: stream.push() after EOF #420

Closed manolobattistadev closed 3 years ago

manolobattistadev commented 5 years ago

OS: Chrome Version 70.0.3538.110 (official Build) (64 bit)

React-pdf version: 1.0.0 "@react-pdf/renderer": "^1.0.0", "@react-pdf/styled-components": "^1.2.0",

Description: With the last update made 7 days ago the console show this error, before the update the pdf works correctly.

_stream_readable.js:271 Uncaught Error: stream.push() after EOF at readableAddChunk (_stream_readable.js:271) at PDFDocument../node_modules/readable-stream/lib/_stream_readable.js.Readable.push (_stream_readable.js:245) at PDFDocument._write (pdfkit.browser.es.js:3731) at PDFReference.finalize (pdfkit.browser.es.js:255) at PDFReference.end (pdfkit.browser.es.js:247) at PNGImage.finalize (pdfkit.browser.es.js:3162) at pdfkit.browser.es.js:3202 at Deflate.onEnd (index.js:225) at Deflate../node_modules/events/events.js.EventEmitter.emit (events.js:96) at endReadableNT (_stream_readable.js:1010) at afterTickTwo (index.js:27) at Item../node_modules/process/browser.js.Item.run (browser.js:153) at drainQueue (browser.js:123)

tobua commented 5 years ago

Had the same error upgrading to 1.0.0 and it was because a second render was accessing the already finished stream from the last render that was still generating the PDF. In my case with React a second render was triggered in componentDidUpdate, while another one triggered by componentDidMount was still running (accessing the same stream) in the background. Solved the issue by creating a sequential queue:

import queue from 'queue'

const renderQueue = queue({
  autostart: true, // Directly start when pushing.
  concurrency: 1 // One concurrent job => run in series.
})

// Without a queue, render would happen in parallel, accessing the same
// stream, which will lead to "Error: stream.push() after EOF".
renderQueue.push(() => renderPDF())
bkoltai commented 5 years ago

I just saw this issue come through from my production app. I'm using the DOM bindings, so have a few levels of abstraction from the renderPDF method. I could see the same case @naminho outlined happening for me as I re-render the PDF when data changes, which may trigger the error if a previous rendering was still inflight.

diegomura commented 5 years ago

Hey. Thanks for reporting this.

Could someone share a snippet I can use to reproduce this issue? I just tried making updates to a document and everything worked well.

This is what I did:

class App extends React.Component {
  state = { counter: 10 }

  componentDidMount() {
    setInterval(() => {
      if (this.state.counter > 0) {
        this.setState({ counter: this.state.counter - 1 })
      }
    }, 2500)
  }

  render() {
    return (
      <PDFViewer style={{ width: '100%', height: '100%' }}>
        <Document>
          <Page>
            <Text>{this.state.counter}</Text>
          </Page>
        </Document>
      </PDFViewer>
    )
  }
}

I basically see the counter being decreased from 10 to 0 in the PDFViewer

Chrome version: 71.0.3578.98 (Official Build) (64-bit)

bkoltai commented 5 years ago

What happens if you set the interval to something really low like 5. I think the issue is when the PDF is still rendering but another render is called. Essentially you need to have two pdf rendering methods overlapping. Might need to make the PDF more complex to slow down the rendering.

On Thu, Dec 13, 2018 at 6:29 PM Diego Muracciole notifications@github.com wrote:

Hey. Thanks for reporting this.

Could someone share a snippet I can use to reproduce this issue? I just tried making updates to a document and everything worked well.

This is what I did:

class App extends React.Component { state = { counter: 10 }

componentDidMount() { setInterval(() => { if (this.state.counter > 0) { this.setState({ counter: this.state.counter - 1 }) } }, 2500) }

render() { return ( <PDFViewer style={{ width: '100%', height: '100%' }}>

{this.state.counter}
  </PDFViewer>
)

} }

I basically see the counter being decreased from 10 to 0 in the PDFViewer

— You are receiving this because you commented.

Reply to this email directly, view it on GitHub https://github.com/diegomura/react-pdf/issues/420#issuecomment-447192014, or mute the thread https://github.com/notifications/unsubscribe-auth/AAcXfDJsUhynDaZJamR4Ne1OmNwd41THks5u4w0hgaJpZM4ZRowr .

artemean commented 5 years ago

I render PDFDownloadLink in table cells. If that table renders only once there is no error. But my table renders twice because I do some DOM calculations. So I'm getting Uncaught Error: stream.push() after EOF. Now I'm stuck how to overcome this :( I don't see how queue can help here. Is there a way to cancel PDF generation in componentWillUnmount maybe? Or any other ideas?

diegomura commented 5 years ago

@bkoltai I tried by also setting the timeout as 5, but still cannot reproduce this. What happens is I get the PDF with a 0 almost immediately. Could someone check if this is still present in the last version?

bkoltai commented 5 years ago

I actually think I narrowed my error usecase down to triggering a re-render from a componentDidMount|Update. Essentially, my component would mount, triggering a render of the PDF, and then immediately trigger a second render due to a state change. I imagine the first render never actually flushed to the DOM and the second render would throw the error, though I'm pretty naive about React internals, so not sure if that's accurate. Anyway, maybe triggering a re-render from a componentDidMount by calling setState might lead to the error, sorry I haven't been able to try for repro myself.

diegomura commented 5 years ago

Thanks @bkoltai, but isn't my example doing what you said? It's basically triggering a re-render on componentDidMount when the counter get's decreased

bkoltai commented 5 years ago

Close I think, but might be different if it was just an immediate setState rather than in a setInterval

On Tue, Jan 15, 2019 at 6:58 PM Diego Muracciole notifications@github.com wrote:

Thanks @bkoltai https://github.com/bkoltai, but isn't my example doing what you said? It's basically triggering a re-render on componentDidMount when the counter get's decreased

— You are receiving this because you were mentioned.

Reply to this email directly, view it on GitHub https://github.com/diegomura/react-pdf/issues/420#issuecomment-454633498, or mute the thread https://github.com/notifications/unsubscribe-auth/AAcXfNQhzcEFInhmqX81XsyYzNnh0L80ks5vDpVNgaJpZM4ZRowr .

diegomura commented 5 years ago

Oh, got it! Going to try that. Thanks!

Vija02 commented 5 years ago

Thanks to @bkoltai comment I manage to do this by making sure only 1 of the PDF will load at a time.
The row renders a button that simply open a Modal. PDF is rendered here only when modal is open and I set shouldComponentUpdate to always return false.

This way there will never be any re-render and only 1 PDF component will render at a time. A bit troublesome but it works

modemmute commented 5 years ago

I am also getting this error but in addition to the above problems my initial pdf document does not contain the properly rendered information. In the below example, this code should render a simple page with text on it but instead renders two blank pages:

import { Document, Page, View, Text, PDFViewer } from '@react-pdf/renderer';
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
const myDoc = (
  <Document>
    <Page>
      <View><Text>Test1</Text></View>
      <View><Text>Test2</Text></View>
      <View><Text>Test3</Text></View>
    </Page>
  </Document>
);
export default class Publish extends React.Component {
  render() {
    const { isOpen, toggle } = this.props;
    return (
      <Modal isOpen={isOpen} toggle={toggle} size={'lg'} className={'settings-modal'} >
        <ModalHeader toggle={toggle}>Publish Test</ModalHeader>
        <ModalBody>
          <PDFViewer width={250} height={'100%'}>
            {myDoc}
          </PDFViewer>
        </ModalBody>
        <ModalFooter>
          <Button onClick={toggle}>Close</Button>
        </ModalFooter>
      </Modal>
    );
  }
}

In a more complicated version of the above code, I have the ability to change the props of the component that renders the pdf. When the props change, the pdf document renders properly.

Noting issue #462 - the error message for that issue on codesandbox is "Potential infinite loop". Given that the above suggestions are about multiple renders causing the "stream.push() after EOF" error - could there be a correlation between these two issues?

I LOVE this library - thank you so much for creating it!

DuncanMacWeb commented 5 years ago

We are also seeing this in Firefox and IE too. @diegomura I wonder if we should look in to @naminho’s insight that “a second render was triggered in componentDidUpdate, while another one triggered by componentDidMount was still running (accessing the same stream) in the background”. I’m not sure where to start following the logic to find which stream is being pushed to after EOF, though.

I think this may (or may not) be related to #476, as we are also not seeing the stream.push() after EOF error on Chrome, where the layout bug in #476 is not happening.

ycruzb commented 5 years ago

Same problem here, including Chrome :(

geeksambhu commented 5 years ago

I faced the same problem while rendering PDFViewer in modal view the pdf thus rendered has blank pages and duplicate content. However, when rendering directly without modal no such issue persists. any update on this?

modemmute commented 5 years ago

I couldn't get this to work in a modal, but I have a temporary hack that makes the page load, even when it's receiving props from another page. In this example I'm sending state from a Link to my Publish page, which generates a PDF.

On the first page we... <Link to={{ pathname: '/publish', state: {foo: 'New Value'}}}>Click Here</Link>

Then that loads the below page...

import React from 'react';
import { PDFViewer, Document, Page, View, Text } from '@react-pdf/renderer'

export default class Publish extends React.Component {
  constructor(props) {
    super(props);

    this.setStateFromLink = this.setStateFromLink.bind(this);

    this.state = {
      ready: true,
      foo: 'Unchanged Value',
    };
  }
  setStateFromLink(state) { // set state from incoming <Link>
    if(state) {
      const { foo } = state 
      this.setState({ foo })
    }    
  }

  componentDidMount() {
    this.setStateFromLink(this.props.location.state)

    // ************************************************************************************
    // BEGIN HACKY BS - wait 1ms for props and state to settle before rendering the PDF
    // react-pdf crashes if a re-render occurs when it's already rendering.

    this.setState({ ready: false });
    setTimeout(()=>{
      this.setState({ ready: true });
    }, 1);

    // END *******************************************************************************

  }
  render() {
    if (this.state.ready) {
      return (
        <React.Fragment>
          <PDFViewer>
            <Document>
              <Page>
                <View>
                  <Text>Foo: {this.state.foo}</Text>
                </View>
              </Page>
            </Document>
          </PDFViewer>
        </React.Fragment>
      )
    } else {
      return null
    }
  }
}

This successfully loads the value from the previous page by pausing the rendering of the Publish page for 1ms while the props & state sort themselves out. Then, with that 1ms passed, the page renders once and react-pdf successfully renders.

THIS IS HACKY! But I suppose it's a temp workaround until this multiple-render issue in react-pdf is resolved. Hope this helps someone (or feel free to tell me that I've broken the universe with this fix and that I shouldn't do it)

manolobattista commented 5 years ago

I couldn't get this to work in a modal, but I have a temporary hack that makes the page load, even when it's receiving props from another page. In this example I'm sending state from a Link to my Publish page, which generates a PDF.

On the first page we... <Link to={{ pathname: '/publish', state: {foo: 'New Value'}}}>Click Here</Link>

Then that loads the below page...

import React from 'react';
import { PDFViewer, Document, Page, View, Text } from '@react-pdf/renderer'

export default class Publish extends React.Component {
  constructor(props) {
    super(props);

    this.setStateFromLink = this.setStateFromLink.bind(this);

    this.state = {
      ready: true,
      foo: 'Unchanged Value',
    };
  }
  setStateFromLink(state) { // set state from incoming <Link>
    if(state) {
      const { foo } = state 
      this.setState({ foo })
    }    
  }

  componentDidMount() {
    this.setStateFromLink(this.props.location.state)

    // ************************************************************************************
    // BEGIN HACKY BS - wait 1ms for props and state to settle before rendering the PDF
    // react-pdf crashes if a re-render occurs when it's already rendering.

    this.setState({ ready: false });
    setTimeout(()=>{
      this.setState({ ready: true });
    }, 1);

    // END *******************************************************************************

  }
  render() {
    if (this.state.ready) {
      return (
        <React.Fragment>
          <PDFViewer>
            <Document>
              <Page>
                <View>
                  <Text>Foo: {this.state.foo}</Text>
                </View>
              </Page>
            </Document>
          </PDFViewer>
        </React.Fragment>
      )
    } else {
      return null
    }
  }
}

This successfully loads the value from the previous page by pausing the rendering of the Publish page for 1ms while the props & state sort themselves out. Then, with that 1ms passed, the page renders once and react-pdf successfully renders.

THIS IS HACKY! But I suppose it's a temp workaround until this multiple-render issue in react-pdf is resolved. Hope this helps someone (or feel free to tell me that I've broken the universe with this fix and that I shouldn't do it)

I tried it now, it seems work correctly 👍

sondretj commented 5 years ago

I solved my version of this issue with shouldComponentUpdate and the onRender callback.

  createDocument = () => (
    <Document onRender={() => { LOADING = false;}}>
      <Page size="A4" style={styles.page}>
        <View style={styles.section}>
          <Text>Section #1</Text>
        </View>
        <View style={styles.section}>
          <Text>Section #2</Text>
        </View>
      </Page>
    </Document>
  );
  shouldComponentUpdate(nextProps, nextState, nextContext) {
    return !LOADING
  }

  render () {
      return  <PDFViewer width={480} height={600}>{this.createDocument()}</PDFViewer>
  }
FranzFlueckiger commented 5 years ago

I solved my version of this issue with shouldComponentUpdate and the onRender callback.

  createDocument = () => (
    <Document onRender={() => { LOADING = false;}}>
      <Page size="A4" style={styles.page}>
        <View style={styles.section}>
          <Text>Section #1</Text>
        </View>
        <View style={styles.section}>
          <Text>Section #2</Text>
        </View>
      </Page>
    </Document>
  );
  shouldComponentUpdate(nextProps, nextState, nextContext) {
    return !LOADING
  }

  render () {
      return  <PDFViewer width={480} height={600}>{this.createDocument()}</PDFViewer>
  }

Could you please release the whole snippet? We're facing the same issue.

sondretj commented 5 years ago

I solved my version of this issue with shouldComponentUpdate and the onRender callback.

  createDocument = () => (
    <Document onRender={() => { LOADING = false;}}>
      <Page size="A4" style={styles.page}>
        <View style={styles.section}>
          <Text>Section #1</Text>
        </View>
        <View style={styles.section}>
          <Text>Section #2</Text>
        </View>
      </Page>
    </Document>
  );
  shouldComponentUpdate(nextProps, nextState, nextContext) {
    return !LOADING
  }

  render () {
      return  <PDFViewer width={480} height={600}>{this.createDocument()}</PDFViewer>
  }

Could you please release the whole snippet? We're facing the same issue.

That was basically it. The LOADING variable was just defined globally. You don't get it to work ? This example will only work for some use cases as it depends on what may cause a re-render and how often etc.

If you are re-rendering and changing the document this workaround could be better:

componentDidUpdate() {
    const doc = this.createDocument();
      ReactDOM.unmountComponentAtNode(document.getElementById('PDF_CONTAINER'));
      if (this.state.width > 0 && this.state.height) {
        ReactDOM.render(
          <>
            <PDFDownloadLink document={doc} fileName="report.pdf">
              {({ blob, url, loading, error }) => (loading ? 'Loading document...' :      'Download')}
            </PDFDownloadLink>
            <PDFViewer width={this.state.width} height={this.state.height}>{doc
            }</PDFViewer>
          </>, document.getElementById('PDF_CONTAINER'))}

  }

render () {
 return <div id="PDF_CONTAINER"  />
}

There are probably better ways. But as I understand it you can't update a document that is still rendering and since the pdf-rendering is async there is no guarantee that it's finished the next time the component want's to render. So either you'll take full control of when it updates ( 1st example) which says only render one at a time, or you just un-mount/re-mount the component with updated props (2nd example).

FranzFlueckiger commented 5 years ago

@sondretj Thank you so much. We've now adapted your solution to React hooks and Functional components for our Typescript based app, and we'd like to share it here:

const Renderer: FC<RendererProps> = props => {
  const [open, setOpen] = useState(false);

  useEffect(() => {
    setOpen(false);
    setOpen(true);
    return () => setOpen(false);
  });

  return (
    <>
      {open && (
        <PDFViewer width={600} height={450}>
          <Document>
            <Page size="A4">{props.children}</Page>
          </Document>
        </PDFViewer>
      )}
    </>
  );
};

export default Renderer;
youngbw commented 5 years ago

I used this method for an on-the-fly pdf download where:

export const createAndDownloadPDF = (pdfContent, filename, divId, callback) => {
    setTimeout(() => {
        const link = (
            <div id={ divId }>
                <PDFDownloadLink document={ pdfContent } fileName={ filename }></PDFDownloadLink>
            </div>
        )
        const elem = document.createElement('div')
        document.getElementById('root').appendChild(elem)
        ReactDOM.render(link, elem)
        setTimeout(() => {
            document.getElementById(divId).children[0].click()
            elem.remove()
            callback()
        }, 1);
    }, 1)
}

Here is the method that is calling it (lives inside a React Component)

buildPDF = () => {
    if (!this.state.loadingPDF) {
    this.setState({ loadingPDF: true }, () => {
           createAndDownloadPDF(this.getPDFContent(), 
                `someName.pdf`, 
                "pdf-creator-link",
                 () => this.setState({ loadingPDF: false })
            )
    })
     }
}

The timeouts in the first method help only in the sense that they remove the render itself from the state cycle, and to let the document render the new div we are appending, respectively. So the setStates Im using for the loading behavior (showing a spinner next to the pdf icon while the PDF content renders) can be set while the pdf render does its thing. Note: this.getPDFContent() just return the Document I want to render using the ReactPDF components

Pros:

Cons:

In general Ive found that if I try to render the PDF while setting state for the component containing it that I get the stream.push() error. This makes it hard to render and hard to update the content, so I just create, render, and download the content on the fly using the above methodology until the core issue is fixed.

egemenuzunali commented 5 years ago

@youngbw I like your hack if you use the render props it gets easier:

<div id="divId">
<PDFDownloadLink document={invoiceTemplate} fileName={invoiceName}>
    {({ blob, url, loading, error }) => {
        if (!loading) {
            setTimeout(() => {
                document.getElementById('divId').children[0].click();
                wipePDf();
            }, 3);
        }
    }}
</PDFDownloadLink>
</div>
benrobertsonio commented 4 years ago

Wow, this thread has been really helpful. I was having similar errors attempting to render PDFDownloadLinks inside of react-inclusive-sortable-table. Every time I went to sort the table, I'd get a WSOD, because it was trying to re-render all the PDFs.

Anyways, I thought it might be helpful for posterity to document a component that puts together what @egemenuzunali and @youngbw were describing above:

import React from 'react';
import { render } from 'react-dom';
import PropTypes from 'prop-types';
import Download from '../table/assets/icon-download.svg';
import { PDFDownloadLink } from '@react-pdf/renderer';
import slugify from 'slugify';
import CertificatePDF from './certificate-pdf';

export default class PDFLink extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      loading: false
    };

    this.generatePDF = this.generatePDF.bind(this);
    this.buildPDF = this.buildPDF.bind(this);
    this.createAndDownloadPDF = this.createAndDownloadPDF.bind(this);
  }

  createAndDownloadPDF(pdfContent, filename, divId, callback) {
    setTimeout(() => {
      const link = (
        <div id={divId}>
          <PDFDownloadLink document={pdfContent} fileName={filename}>
            {({ loading }) => {
              if (!loading) {
                setTimeout(() => {
                  document.getElementById(divId).children[0].click();
                  callback();
                }, 3);
              }
            }}
          </PDFDownloadLink>
        </div>
      );
      const elem = document.createElement('div');
      document.getElementById('___gatsby').appendChild(elem);
      render(link, elem);
    }, 50);
  }

  buildPDF() {
    if (!this.state.loading) {
      this.setState({ loading: true }, () => {
        this.createAndDownloadPDF(
          this.generatePDF(),
          `${slugify(this.props.title).toLowerCase()}.pdf`,
          `${slugify(this.props.title).toLowerCase()}`,
          () => this.setState({ loading: false })
        );
      });
    }
  }

  generatePDF() {
    // CertificatePDF is a component that returns a PDF <Document />
    return <CertificatePDF {...this.props} />;
  }
  render() {
    return this.state.loading ? (
      'Loading...'
    ) : (
      <button onClick={this.buildPDF}>
        <Download
          title={`Click here to download a certificate for ${this.props.title}`}
          height="15"
          width="12"
        />
      </button>
    );
  }
}

PDFLink.propTypes = {
  /* User name on the certificate */
  name: PropTypes.string.isRequired,
  /* Title of the course */
  title: PropTypes.string.isRequired,
  /* Date of completion */
  date: PropTypes.string.isRequired,
  /* Number of credits earned */
  credits: PropTypes.string.isRequired
};
Amritpd commented 4 years ago

Any update on a proper sol'n for rendering the PDF in a modal? Thanks.

EDIT: Managed to implement a solution similar to something suggested above. Isolating the PDFViewer and throwing it in a child component and setting shouldComponentUpdate() equal to false seemed to have worked.

kbrah commented 4 years ago

I had similar issue when using PDFDownloadLink. I solved it with using memo on the PDFDownloadLink.

const Link = (props) => {
    return (
        <PDFDownloadLink
            fileName="file.pdf"
            document={<Doc {...props} />}
        >
            Link
        </PDFDownloadLink>
    );
};

export const PDFLink = memo(Link, (prevProps, newProps) => {
    //compare props and if no change, return true, else false
})
benrobertsonio commented 4 years ago

@kbrah - where does the memo() function come from?

kbrah commented 4 years ago

@benjamingrobertson It's included in React import {memo} from 'react

iaurg commented 4 years ago

Hi everyone,

I did the same like @benjamingrobertson only removing dependent parent divs and updating some things.

Just copy and paste:

https://gist.github.com/Italox/c1d53dd58d9a2a170272b469d9edbd79

Thx @benjamingrobertson 👍

paramsinghvc commented 4 years ago

Is there any other alternative to this library guys?? Can't spend hours now just to hack around it.

chemicalkosek commented 4 years ago

Is there any other alternative to this library guys?? Can't spend hours now just to hack around it.

I was frustrated just like you with this and other issues. Once I hacked around them, this library was very convenient to use. I don't know what are you trying to accomplish, but maybe use Puppeteer https://github.com/GoogleChrome/puppeteer

iaurg commented 4 years ago

Is there any other alternative to this library guys?? Can't spend hours now just to hack around it.

@paramsinghvc I've tried other, but despite of hack this is the better pdf generator in front-end. Can you try react-to-pdf, but this module take a screenshot of a children (ref) component. Maybe it work on your project.

benrobertsonio commented 4 years ago

I just implemented React.memo as @kbrah suggested, and it is a much cleaner (and less buggy) solution.

thanks @kbrah!!

Unionindesign commented 4 years ago

It sounds like we've all had similar issues, and have arrived at similar solutions that all involve waiting for the data, either with some hackier-setTimeout functions (not criticizing - been there!)...my approach has been conditional rendering. Even when the app was working before this I was getting some fun errors, including the 'cannot setState on an unmounted component' etc. These all seem to be gone just by removing PDFViewer or PDFDownloadLink from the initial page load.

I'd be happy to show some code...my app chains together a whole mess of API calls using useEffect and async await etc...has several loading and error states, it would be a lot...

Save yourself the trouble - don't put the PDFViewer or PDFDownloadLink components right on the page with initial load! I've set up an initial view to return a div that has a form that asks for user input to fetch data first, or has a button you can click to use test data...that's it! Then it will return something else during loading, and then when I've set the state for loading back to false after the datga has arrived, only then will the app return the option for the user to preview or download. Seems to be working so far! Conditionally render everything!

devinhalladay commented 4 years ago

Running into this same error with a different use case. I'm using react-router-dom to redirect a user to a user-specified slug (i.e. http://site.com/${slug}). I use a Switch to control route rendering, and at the path /:slug, my app renders the PDFViewer component. The redirect does not happen until after all my data is loaded and added to the state.

Everything works fine the first time the route is rendered, but subsequent renders on that route fail with this error (regardless of whether the route is navigated to from / OR /:slug).

I'm not exactly sure what the issue is in this case. Conditionally rendering the PDFViewer based on a loading state key doesn't solve the issue for me. Any ideas?

EDIT: The relevant code is here

kbrah commented 4 years ago

@devinhalladay have you tried wrapping the Viewer component in React.memo in the same way I did PDFDownloadLink? (My previous answer)

devinhalladay commented 4 years ago

@kbrah just tried it and didn't have much luck (though it's very possible I implemented React.memo incorrectly; still sort of new to React). However, this solution from @artemean just worked for me!

devinhalladay commented 4 years ago

It's referenced above but I think #736 is related to this issue. I'm building out an interface that lets users control what things are rendered in the pdf, and neither React.memo nor returning false from shouldComponentUpdate are helping (the latter worked at first, until I needed to update props on the PDFViewer). Seems like the trick is to do everything on-demand, but this doesn't work when PDFViewer is already rendered and needs to be re-rendered several times. Could unmounting and remounting the PDFViewer component work?

EDIT: okay, the solution that has been most successful for my use case is to thread a pdfKey prop down through to my memoized PDFViewer, and implement it as <PDFViewer key={this.props.pdfKey}>...</PDFViewer>. Interaction with my control interface triggers a setState() on the parent component, which generates a new pdfKey and passes it back down the tree to re-generate the pdf component.

Hope that's useful for others with a similar use case!

juanruben commented 4 years ago

I couldn't get this to work in a modal, but I have a temporary hack that makes the page load, even when it's receiving props from another page. In this example I'm sending state from a Link to my Publish page, which generates a PDF.

On the first page we... <Link to={{ pathname: '/publish', state: {foo: 'New Value'}}}>Click Here</Link>

Then that loads the below page...

import React from 'react';
import { PDFViewer, Document, Page, View, Text } from '@react-pdf/renderer'

export default class Publish extends React.Component {
  constructor(props) {
    super(props);

    this.setStateFromLink = this.setStateFromLink.bind(this);

    this.state = {
      ready: true,
      foo: 'Unchanged Value',
    };
  }
  setStateFromLink(state) { // set state from incoming <Link>
    if(state) {
      const { foo } = state 
      this.setState({ foo })
    }    
  }

  componentDidMount() {
    this.setStateFromLink(this.props.location.state)

    // ************************************************************************************
    // BEGIN HACKY BS - wait 1ms for props and state to settle before rendering the PDF
    // react-pdf crashes if a re-render occurs when it's already rendering.

    this.setState({ ready: false });
    setTimeout(()=>{
      this.setState({ ready: true });
    }, 1);

    // END *******************************************************************************

  }
  render() {
    if (this.state.ready) {
      return (
        <React.Fragment>
          <PDFViewer>
            <Document>
              <Page>
                <View>
                  <Text>Foo: {this.state.foo}</Text>
                </View>
              </Page>
            </Document>
          </PDFViewer>
        </React.Fragment>
      )
    } else {
      return null
    }
  }
}

This successfully loads the value from the previous page by pausing the rendering of the Publish page for 1ms while the props & state sort themselves out. Then, with that 1ms passed, the page renders once and react-pdf successfully renders.

THIS IS HACKY! But I suppose it's a temp workaround until this multiple-render issue in react-pdf is resolved. Hope this helps someone (or feel free to tell me that I've broken the universe with this fix and that I shouldn't do it)

This hack worked for me very good! Thanks a lot. It works for both PDFViewer and PDFDownladLink in the same Modal.

That you sooooo much!!

juanruben commented 4 years ago

This is my code for this and it works. Thanks to @modemmute

import React, { Component } from 'react';
import ReactPDF, {
    Document, Page, Text, View, StyleSheet, PDFViewer, PDFDownloadLink,
} from '@react-pdf/renderer';
import {
    Modal, ModalHeader, ModalBody, ModalFooter,
} from 'reactstrap';

class MyComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            showing: false,
            ready: false,
        };

        this.toggle = this.toggle.bind(this);
    }

    toggle() {
        this.setState((prevState) => ({
            showing: !prevState.showing,
            ready: false,
        }), () => {     // THIS IS THE HACK
            setTimeout(() => {
                this.setState({ ready: true });
            }, 1);
        });
    }

    render() {
        const { showing, ready } = this.state;

        const styles = StyleSheet.create({
            page: {
                flexDirection: 'row',
                backgroundColor: '#FFF',
            },
            section: {
                margin: 10,
                padding: 10,
                flexGrow: 1,
                fontSize: 100,
                fontWeight: 700,
                color: '#CCC',
            },
            text: {
                color: '#F00',
                margin: 10,
                padding: 10,
                flexGrow: 1,
                fontSize: 12,
            },
        });

        const doc = (
            <Document>
                <Page size="A4" style={styles.page}>
                    <View style={styles.section}>
                        <Text>This is a title</Text>
                    </View>
                </Page>
            </Document>
        );

        return (
            <>
                <Modal isOpen={showing} toggle={this.toggle}>
                    <ModalHeader toggle={this.toggle} />
                    <ModalBody>
                        {ready && (
                            <PDFViewer>
                                {doc}
                            </PDFViewer>
                        )}
                    </ModalBody>
                    <ModalFooter>
                        {ready && (
                            <PDFDownloadLink document={doc} fileName="test.pdf">
                                {
                                    ({ blob, url, loading, error }) => (loading ? 'Loading document...' : 'Download')
                                }
                            </PDFDownloadLink>
                        )}
                    </ModalFooter>
                </Modal>
            </>
        );
    }
}

export default MyComponent;
Archymede123 commented 4 years ago

After quite some time struggling, I've used a PureComponent and it works perfectly. Since Pure Components don't re-render blindly (by design), so does the PDFDownloadLink component. Hope it can help !

cbutton01 commented 4 years ago

Same problem when using functional component to return PDFDownloadLink. Component works fine when the Document only has a page component as a child, but is the page has children it fails to render

youbek commented 4 years ago

In our case, the document component had required prop "data", which could be changed async. So, we assumed that while PDFDownloadLink is loading, it was getting new data, which caused it to rerender before finishing initial loading, so our solution was, to not render at all PDFDownloadLink until final data is there.

{
  (() => {
    if (!reports.length) {
      return null;
    }

    return (
      <PDFDownloadLink
        className="btn btn-primary"
        document={<ReportPDF reports={reports} />}
        fileName="report.pdf"
      >
        {({ blob, url, loading, error }) =>
          loading ? "Loading document..." : "Download PDF"
        }
      </PDFDownloadLink>
    );
  })();
}
phoenixTW commented 4 years ago
export const PDFLayout: React.SFC<PDFLayoutProps> = React.memo(({ children }) => {
    const getLink = (link: string): any => window.open(link, '_blank') && <span />,
        [ready, setReady] = useState(false);

    return (
        <PDFDownloadLink document={children} fileName="somefile.pdf">
            {({ url, loading }) => {
                if (!loading && !ready) {
                    setReady(true);
                    return null;
                }
                if (!loading && ready) {
                    return getLink(url);
                }
            }}
        </PDFDownloadLink>
    );
});

I am still getting the error. Can someone help me what I am missing?

youbek commented 4 years ago

@phoenixTW I've never worked with the memo, but this code should work

export const PDFLayout: React.SFC<PDFLayoutProps> = React.memo(({ children }) => {
    const getLink = (link: string): any => window.open(link, '_blank') && <span />,
        [ready, setReady] = useState(false);

    if(!ready){
        return null; // OR A PLACEHOLDER BUTTON
    }

    return (
        <PDFDownloadLink document={children} fileName="somefile.pdf">
            {({ url, loading }) => {
                 return getLink(url);
            }}
        </PDFDownloadLink>
    );
});

if not ready don't return PDFDownloadLink at all, just return null or a placeholder button. Then if everything is ready, then render PDFDownloadLink

sebastijandumancic commented 4 years ago

Hey guys, there's a lot of code snippets above, but I've managed to fix the issue simply by putting dummy button that says 'Generate PDF' which triggers ready state, and with if check loaded PDFDownloadLink. Otherwise it would just rerender every time a component loads, which brought up the error. Hope this helps someone.

{ready ? (
                  <PDFDownloadLink
                    fileName={`${listing.title} - Investors Club Report`}
                    document={<DetailsPdf listing={listing} graphs={graphs} />}
                  >
                    {({ loading }) =>
                      loading ? (
                        <Loading isLoading />
                      ) : (
                        <div className="btn btn--primary btn--med btn--full">
                          Download now!
                        </div>
                      )
                    }
                  </PDFDownloadLink>
                ) 
DeepakKapiswe commented 4 years ago

I figured out, it was the data change which was causing this problem, so I used dcopy from library deep-copy, I first deep copied the data to be passed to pdf creator / viewer and then this problem is solved, actually it might be trying to render while the fetched data not yet returned completely. Anyway you can try this too if it works, for me I got rid of this frustrating situation :) Thanks for this nice Library :)

vanmoortel commented 4 years ago

Here's how I fixed the problem, I hope it will help others.

Cannot read property 'dereference' of undefined

const GeneratePDF = props => {
  const [isShowBlobProvider, setIsShowBlobProvider] = useState(false);

  const downloadFullPDF = (blob, filename) => {
      const a = document.createElement('a');
      document.body.appendChild(a);
      a.style = 'display: none';
      a.href = window.URL.createObjectURL(blob);
      a.download = filename;
      a.click();
      window.URL.revokeObjectURL(a.href);
      a.remove();
      setIsShowBlobProvider(false);
  };

  return (
    <div>
      {
        isShowBlobProvider?  (
          <BlobProvider document={<Document />} fileName="somename.pdf">
          {({ blob, url, loading, error }) => {
            if (!loading) downloadFullPDF(blob, 'somename.pdf');
            return (
              <Button disabled variant="text" color="primary">
                 <CircularProgress size={24}  />
                 Exporting
               </Button>
             );
          }
        )
      }
      <Button onClick={() => setIsShowBlobProvider(true)} variant="text" color="primary">
        Export
      </Button>
    </div>
  );
};
yfchina143 commented 4 years ago

Thanks @bkoltai, but isn't my example doing what you said? It's basically triggering a re-render on componentDidMount when the counter get's decreased

hi i am new to react but i got this problem as well. could you explain how to set a re-render on componentDidMount?

Rootwd commented 4 years ago

This is my code for this and it works. Thanks to @modemmute

import React, { Component } from 'react';
import ReactPDF, {
    Document, Page, Text, View, StyleSheet, PDFViewer, PDFDownloadLink,
} from '@react-pdf/renderer';
import {
    Modal, ModalHeader, ModalBody, ModalFooter,
} from 'reactstrap';

class MyComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            showing: false,
            ready: false,
        };

        this.toggle = this.toggle.bind(this);
    }

    toggle() {
        this.setState((prevState) => ({
            showing: !prevState.showing,
            ready: false,
        }), () => {     // THIS IS THE HACK
            setTimeout(() => {
                this.setState({ ready: true });
            }, 1);
        });
    }

    render() {
        const { showing, ready } = this.state;

        const styles = StyleSheet.create({
            page: {
                flexDirection: 'row',
                backgroundColor: '#FFF',
            },
            section: {
                margin: 10,
                padding: 10,
                flexGrow: 1,
                fontSize: 100,
                fontWeight: 700,
                color: '#CCC',
            },
            text: {
                color: '#F00',
                margin: 10,
                padding: 10,
                flexGrow: 1,
                fontSize: 12,
            },
        });

        const doc = (
            <Document>
                <Page size="A4" style={styles.page}>
                    <View style={styles.section}>
                        <Text>This is a title</Text>
                    </View>
                </Page>
            </Document>
        );

        return (
            <>
                <Modal isOpen={showing} toggle={this.toggle}>
                    <ModalHeader toggle={this.toggle} />
                    <ModalBody>
                        {ready && (
                            <PDFViewer>
                                {doc}
                            </PDFViewer>
                        )}
                    </ModalBody>
                    <ModalFooter>
                        {ready && (
                            <PDFDownloadLink document={doc} fileName="test.pdf">
                                {
                                    ({ blob, url, loading, error }) => (loading ? 'Loading document...' : 'Download')
                                }
                            </PDFDownloadLink>
                        )}
                    </ModalFooter>
                </Modal>
            </>
        );
    }
}

export default MyComponent;

Please Help i got this error


Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
in InternalBlobProvider (created by PDFViewer)