wojtekmaj / react-pdf

Display PDFs in your React app as easily as if they were images.
https://projects.wojtekmaj.pl/react-pdf
MIT License
9.11k stars 867 forks source link

ArrayBuffer detached when upgrading from react-pdf from 6.2.2 to 7.5.1 #1657

Open Meess opened 9 months ago

Meess commented 9 months ago

Before you start - checklist

Description

When migrating from 6.2.2 to 7.5.1 the arrayBuffer becomes detached after use of <Document ... file={arrayBuffer} ... >. This can lead to some weird behaviour when arrayBuffer is reused (e.g. by using it as input react-pdf ).

I suspect the arrayBuffer reference is used as caching id, while array buffer becomes detached in 7.5.1.

I've tried a few workarounds, but one issue alway remains in 7.5.1 compared to 6.2.2: when loading a Pdf in 7.5.1 it always goes into loading state, instead of in 6.2.2 where, when using an ArrayBuffer, no loading state is shown when using the ArrayBuffer a second time after the component it was used in was unmounted. I don't know if this is intentional due to changes from 6.2.2 to 7.5.1 and underlying pdf.js library, as I can't test it in 7.5.1 with the original array buffer which becomes detached after first use.

Steps to reproduce

Now let's assume some hooks will go off, e.g. for setting the number of pages on load

Now do something that unmounts the component, e.g. move to a different page, or open another document, and then go back to the page with the original pdf of this example.

Expected behavior

Expected the arrayBuffer to not be detached in 7.5.1, similar to 6.2.2

Actual behavior

arrayBuffer is detached in 7.5.1, deviating in behaviour from 6.2.2

Additional information

Workarounds I tried:

Make a blob out of it

Make a copy of the array buffer on each render, and provide the copy to <Document ...> to the original is never detached:

const copyArrayBuffer = (arrayBuffer: ArrayBuffer) => {
    const copiedArrayBuffer = new ArrayBuffer(arrayBuffer.byteLength);
    new Uint8Array(copiedArrayBuffer).set(new Uint8Array(arrayBuffer));
    return copiedArrayBuffer;
};

Make a Uint8Array of it:

Environment

Meess commented 9 months ago

A function / hook that preprocesses the File / ArrayBuffer which returns an intermediate representation that can be directly inserted in , which would effectively reduce or better remove the loading state all together.

Then the processing/preprocessing steps only have to happen once, just after the data is fetched, and not on rerenders or when the component is mounted again.

Adelrisk commented 8 months ago

@Meess Do any of your workarounds work? I believe I am being affected by this.

Ideally, I don't want to re-trigger the loading process if the data doesn't change, but I don't know how to achieve this.

Adelrisk commented 8 months ago

I believe I may have solved this issue given a hint from the documentation:

Make sure to define options object outside of your React component, and use useMemo if you can't.

The buffer is still being detached (length zero), but this doesn't lead to an error.

croraf commented 8 months ago

I experience the same issue. After providing ArrayBuffer to the Document it has length 0 (no data).

As a workaround I have to do a copy of the old buffer (same as mentioned in the OP)

function copyArrayBuffer(originalBuffer) {
  // Create a new ArrayBuffer with the same byte length as the original
  const newBuffer = new ArrayBuffer(originalBuffer.byteLength);

  // Create TypedArray views for both the original and new buffers
  const originalView = new Uint8Array(originalBuffer);
  const newView = new Uint8Array(newBuffer);

  // Copy the data from the original buffer to the new buffer
  newView.set(originalView);

  return newBuffer;
}
Meess commented 7 months ago

@Meess Do any of your workarounds work? I believe I am being affected by this.

Ideally, I don't want to re-trigger the loading process if the data doesn't change, but I don't know how to achieve this.

@Adelrisk no we didn't find a workaround and stayed on 6.2.2 for the last few months, but are now accepting our faith and are migrating to 7.x . I went for the "Make a blob out of it" workaround to fix the empty arraybuffer, unfortunately that doesn't resolve the re-transformation of the array buffer to an actual pdf <Document file={blob} loading={<>Some loading state...</>} >...

I'm fetching the pdf as blob from our server now, with axios, so the arraybuffer won't be empty after the component is unmounted and mounted again:

const fetchPDF = async (url: string) => {
    const response = await axios.get<Blob>(url, { responseType: 'blob' });
    return response.data;
};

About useMemo, I'm not sure what improvement that will give as useMemo in React only memoizes from the previous render. So if your react component unmounts and then later mounts on a different page the arraybuffer will be 0 again.

At the moment I don't see a workaround for this as the library itself loads the pdf again if it's a blob in react-pdf/packages/react-pdf/src/Document.tsx

if (isBrowser) {
      // File is a Blob
      if (isBlob(file)) {
        const data = await loadFromFile(file);

        return { data };
      }
    }
Adelrisk commented 7 months ago

@Meess My above answer solved this issue for me. As code can be clearer than words.

bad:

import { pdfjs, Document } from "react-pdf";

export const PDFViewer = ({ pdflike }) => {
  // ...
  // The options inside the component are the problem
  const options = {
    // ...
  };
  return <Document file={pdflike} options={options}>
    <!-- ... -->
   </Document>
};

good:

import { pdfjs, Document, Outline, Page } from "react-pdf";

// The options outside the component fixed my problem.
const options = {
    // ...
};
export const PDFViewer = ({ pdflike }) => {
  // ...
  return <Document file={pdflike} options={options}>
    <!-- ... -->
   </Document>
};

I strongly suspect that the creation of a new options object on each render causes a re-rendering of the pdf component, because each object is not identical (in Javascript, {} != {} is always true). The pdf-rendering stack then breaks down, stuff goes wrong and a previously clean-up ArrayBuffer is accesses inappropriately causes the above error.

Meess commented 7 months ago

@Adelrisk I fully glanced over the "options" part, as I'm not using any options. So we might have a different issue! I'm not experiencing any issues with re-rendering of the component (i.e. the component stays mounted, but rerenders). But good to know about the "options" in case I start using it in the future, thanks!

When using 6.2.x react-pdf and using an ArrayBuffer as input for the pdf it behaved differently than 7.x.

github-actions[bot] commented 3 months ago

This issue is stale because it has been open 90 days with no activity. Remove stale label or comment or this issue will be closed in 14 days.

obecker commented 3 months ago

I'm facing this issue during development, so it's not a production issue, but seeing this constantly after changing something in the sources of my react app is quite annoying.

In my case the PDF is opened as a local file in the browser. It renders fine, however, when i change something in the sources and the app gets automatically updated, I'm seeing an empty browser window with the mentioned errors in the console.

github-actions[bot] commented 3 weeks ago

This issue is stale because it has been open 90 days with no activity. Remove stale label or comment or this issue will be closed in 14 days.

AlexSchwabauer commented 1 week ago

Facing the same issue on dev and production