diff --git a/packages/react-pdf/package.json b/packages/react-pdf/package.json
index 1fed6582f..558d69b4f 100644
--- a/packages/react-pdf/package.json
+++ b/packages/react-pdf/package.json
@@ -56,6 +56,7 @@
"license": "MIT",
"dependencies": {
"clsx": "^2.0.0",
+ "dequal": "^2.0.3",
"make-cancellable-promise": "^1.3.1",
"make-event-props": "^1.6.0",
"merge-refs": "^1.2.1",
diff --git a/packages/react-pdf/src/Document.spec.tsx b/packages/react-pdf/src/Document.spec.tsx
index 30012b09d..b2223a148 100644
--- a/packages/react-pdf/src/Document.spec.tsx
+++ b/packages/react-pdf/src/Document.spec.tsx
@@ -604,4 +604,92 @@ describe('Document', () => {
expect(onTouchStart).toHaveBeenCalled();
});
+
+ it('does not warn if file prop was memoized', () => {
+ const spy = vi.spyOn(global.console, 'error').mockImplementation(() => {
+ // Intentionally empty
+ });
+
+ const file = { data: pdfFile.arrayBuffer };
+
+ const { rerender } = render();
+
+ rerender();
+
+ expect(spy).not.toHaveBeenCalled();
+
+ vi.mocked(global.console.error).mockRestore();
+ });
+
+ it('warns if file prop was not memoized', () => {
+ const spy = vi.spyOn(global.console, 'error').mockImplementation(() => {
+ // Intentionally empty
+ });
+
+ const { rerender } = render();
+
+ rerender();
+
+ expect(spy).toHaveBeenCalledTimes(1);
+
+ vi.mocked(global.console.error).mockRestore();
+ });
+
+ it('does not warn if file prop was not memoized, but was changed', () => {
+ const spy = vi.spyOn(global.console, 'error').mockImplementation(() => {
+ // Intentionally empty
+ });
+
+ const { rerender } = render();
+
+ rerender();
+
+ expect(spy).not.toHaveBeenCalled();
+
+ vi.mocked(global.console.error).mockRestore();
+ });
+
+ it('does not warn if options prop was memoized', () => {
+ const spy = vi.spyOn(global.console, 'error').mockImplementation(() => {
+ // Intentionally empty
+ });
+
+ const options = {};
+
+ const { rerender } = render();
+
+ rerender();
+
+ expect(spy).not.toHaveBeenCalled();
+
+ vi.mocked(global.console.error).mockRestore();
+ });
+
+ it('warns if options prop was not memoized', () => {
+ const spy = vi.spyOn(global.console, 'error').mockImplementation(() => {
+ // Intentionally empty
+ });
+
+ const { rerender } = render();
+
+ rerender();
+
+ expect(spy).toHaveBeenCalledTimes(1);
+
+ vi.mocked(global.console.error).mockRestore();
+ });
+
+ it('does not warn if options prop was not memoized, but was changed', () => {
+ const spy = vi.spyOn(global.console, 'error').mockImplementation(() => {
+ // Intentionally empty
+ });
+
+ const { rerender } = render();
+
+ rerender();
+
+ expect(spy).not.toHaveBeenCalled();
+
+ vi.mocked(global.console.error).mockRestore();
+ });
});
diff --git a/packages/react-pdf/src/Document.tsx b/packages/react-pdf/src/Document.tsx
index 09dd4d5e1..0116d681c 100644
--- a/packages/react-pdf/src/Document.tsx
+++ b/packages/react-pdf/src/Document.tsx
@@ -14,6 +14,7 @@ import makeCancellable from 'make-cancellable-promise';
import clsx from 'clsx';
import invariant from 'tiny-invariant';
import warning from 'warning';
+import { dequal } from 'dequal';
import pdfjs from './pdfjs.js';
import DocumentContext from './DocumentContext.js';
@@ -233,6 +234,14 @@ const defaultOnPassword: OnPassword = (callback, reason) => {
}
};
+function isParameterObject(file: File): file is Source {
+ return (
+ typeof file === 'object' &&
+ file !== null &&
+ ('data' in file || 'range' in file || 'url' in file)
+ );
+}
+
/**
* Loads a document passed using `file` prop.
*/
@@ -271,6 +280,32 @@ const Document = forwardRef(function Document(
const pages = useRef([]);
+ const prevFile = useRef();
+ const prevOptions = useRef();
+
+ useEffect(() => {
+ if (file && file !== prevFile.current && isParameterObject(file)) {
+ warning(
+ !dequal(file, prevFile.current),
+ `File prop passed to changed, but it's equal to previous one. This might result in unnecessary reloads. Consider memoizing the value passed to "file" prop.`,
+ );
+
+ prevFile.current = file;
+ }
+ }, [file]);
+
+ // Detect non-memoized changes in options prop
+ useEffect(() => {
+ if (options && options !== prevOptions.current) {
+ warning(
+ !dequal(options, prevOptions.current),
+ `Options prop passed to changed, but it's equal to previous one. This might result in unnecessary reloads. Consider memoizing the value passed to "options" prop.`,
+ );
+
+ prevOptions.current = options;
+ }
+ }, [options]);
+
const viewer = useRef({
// Handling jumping to internal links target
scrollPageIntoView: (args: ScrollPageIntoViewArgs) => {
@@ -385,7 +420,7 @@ const Document = forwardRef(function Document(
);
invariant(
- 'data' in file || 'range' in file || 'url' in file,
+ isParameterObject(file),
'Invalid parameter object: need either .data, .range or .url',
);
diff --git a/yarn.lock b/yarn.lock
index 900c15b27..8fa1fd42a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4528,6 +4528,7 @@ __metadata:
canvas: "npm:^2.11.2"
clsx: "npm:^2.0.0"
cpy-cli: "npm:^5.0.0"
+ dequal: "npm:^2.0.3"
eslint: "npm:^8.26.0"
eslint-config-wojtekmaj: "npm:^0.9.0"
jsdom: "npm:^21.1.0"