14

I am using a React PDF viewer in my project. I have a react mui dialog component that I use with react draggable to drag it around.

import React from "react";
import withStyles from "@material-ui/core/styles/withStyles";
import makeStyles from "@material-ui/core/styles/makeStyles";
import DialogContent from "@material-ui/core/DialogContent";
import IconButton from "@material-ui/core/IconButton";
import ClearIcon from "@material-ui/icons/Clear";
import Draggable from "react-draggable";
import Paper from "@material-ui/core/Paper";
import Dialog from "@material-ui/core/Dialog";
import PDFViewer from "./PDFViewer";

function PaperComponent({...props}) {
  return (
    <Draggable
    >
      <Paper {...props} />
    </Draggable>
  );
}

const StyledDialog = withStyles({
  root: {
    pointerEvents: "none"
  },
  paper: {
    pointerEvents: "auto"
  },
  scrollPaper: {
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'flex-end',
    marginRight: 20
  }
})(props => <Dialog hideBackdrop {...props} />);

const useStyles = makeStyles({
  dialog: {
    cursor: 'move'
  },
  dialogContent: {
    '&:first-child': {
      padding: 10,
      background: 'white'
    }
  },
  clearIcon: {
    position: 'absolute',
    top: -20,
    right: -20,
    background: 'white',
    zIndex: 1,
    '&:hover': {
      background: 'white'
    }
  },
  paper: {
    overflowY: 'visible',
    maxWidth: 'none',
    maxHeight: 'none',
    width: 550,
    height: 730
  }
});

const PDFModal = (props) => {
  const classes = useStyles();
  const {open, onClose, pdfURL} = props;
  return (
    <StyledDialog
      open={open}
      classes={{root: classes.dialog, paper: classes.paper}}
      PaperComponent={PaperComponent}
      aria-labelledby="draggable-dialog"
    >
      <DialogContent classes={{root: classes.dialogContent}} id="draggable-dialog">
        <IconButton className={classes.clearIcon} aria-label="Clear" onClick={onClose}>
          <ClearIcon/>
        </IconButton>
        <PDFViewer
          url={pdfURL}
        />
      </DialogContent>
    </StyledDialog>
  );
};


export default PDFModal;

And this is the PDFViewer component:

import React from 'react';
import { Viewer, SpecialZoomLevel, Worker  } from '@react-pdf-viewer/core';
import { defaultLayoutPlugin } from '@react-pdf-viewer/default-layout';

import '@react-pdf-viewer/core/lib/styles/index.css';
import '@react-pdf-viewer/default-layout/lib/styles/index.css';
import ArrowForward from "@material-ui/icons/ArrowForward";
import ArrowBack from "@material-ui/icons/ArrowBack";
import Button from "@material-ui/core/Button";
import IconButton from "@material-ui/core/IconButton";
import RemoveCircleOutlineIcon from '@material-ui/icons/RemoveCircleOutline';
import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline';
import './PDFViewer.css';

const PDFViewer = ({url}) => {
  const renderToolbar = (Toolbar) => (
    <Toolbar>
      {
        (slots) => {
          const {
            CurrentPageLabel, CurrentScale, GoToNextPage, GoToPreviousPage, ZoomIn, ZoomOut,
          } = slots;
          return (
            <div
              style={{
                alignItems: 'center',
                display: 'flex',
              }}
            >
              <div style={{ padding: '0px 2px' }}>
                <ZoomOut>
                  {
                    (props) => (
                      <IconButton aria-label="delete" onClick={props.onClick}>
                        <RemoveCircleOutlineIcon />
                      </IconButton>
                    )
                  }
                </ZoomOut>
              </div>
              <div style={{ padding: '0px 2px' }}>
                <CurrentScale>
                  {
                    (props) => (
                      <span>{`${Math.round(props.scale * 100)}%`}</span>
                    )
                  }
                </CurrentScale>
              </div>
              <div style={{ padding: '0px 2px' }}>
                <ZoomIn>
                  {
                    (props) => (
                      <IconButton aria-label="delete" onClick={props.onClick}>
                        <AddCircleOutlineIcon />
                      </IconButton>
                    )
                  }
                </ZoomIn>
              </div>
              <div style={{ padding: '0px 2px', marginLeft: 'auto' }}>
                <GoToPreviousPage>
                  {
                    (props) => (
                      <Button
                        style={{
                          cursor: props.isDisabled ? 'not-allowed' : 'pointer',
                          height: '30px',
                          width: '30px'
                        }}
                        disabled={props.isDisabled}
                        disableElevation
                        disableFocusRipple
                        onClick={props.onClick}
                        variant="outlined">
                        <ArrowBack fontSize="small"/>
                      </Button>
                    )
                  }
                </GoToPreviousPage>
              </div>
              <div style={{ padding: '0px 2px' }}>
                <CurrentPageLabel>
                  {
                    (props) => (
                      <span>{`${props.currentPage + 1} av ${props.numberOfPages}`}</span>
                    )
                  }
                </CurrentPageLabel>
              </div>
              <div style={{ padding: '0px 2px' }}>
                <GoToNextPage>
                  {
                    (props) => (
                      <Button
                        style={{
                          cursor: props.isDisabled ? 'not-allowed' : 'pointer',
                          height: '30px',
                          width: '30px'
                        }}
                        disabled={props.isDisabled}
                        disableElevation
                        disableFocusRipple
                        onClick={props.onClick}
                        variant="outlined">
                        <ArrowForward fontSize="small"/>
                      </Button>
                    )
                  }
                </GoToNextPage>
              </div>
            </div>
          )
        }
      }
    </Toolbar>
  );

  const defaultLayoutPluginInstance = defaultLayoutPlugin({
    renderToolbar,
    sidebarTabs: defaultTabs => [defaultTabs[1]]
  });

  // constantly called
  console.log('entered')
  return (
    <div
      style={{
        height: '100%',
      }}
    >
      <Worker workerUrl="https://unpkg.com/pdfjs-dist@2.5.207/build/pdf.worker.min.js">
        <Viewer
          fileUrl={url}
          defaultScale={SpecialZoomLevel.PageFit}
          plugins={[
            defaultLayoutPluginInstance
          ]}
        />
      </Worker>
    </div>
  );
};

export default PDFViewer;

I can see in the console that PDFViewer is being constantly called. I am not sure what is causing this rerenders the whole time?

Ludwig
  • 1,401
  • 13
  • 62
  • 125
  • 2
    I would try defining your `renderToolbar` function outside of the `PDFViewer` and see if that helps. – Linda Paiste Jan 30 '21 at 01:05
  • Can you provide a codesandbox ? – Mohamed Ramrami Feb 01 '21 at 00:35
  • @Macro I tried the same in CodeSandbox: https://codesandbox.io/s/determined-stonebraker-h7emw?file=/src/PDFViewer.js and all I could see is it's not invoking the `console.log(entered)` frequently. `(FYI - I have changed console.log(entered) to console.error(entered) in that codesandbox)` – Nagaraj Tantri Feb 04 '21 at 12:12

1 Answers1

2

Isn't it make sense to re-render when you have a new fileUrl passed to PDFModal? The following sequence should be how the app is executed.

  1. PDFModal, PDFViewer and other related components init
  2. When a file is dragged into the PaperComponent context, the upper level component handles it and passing pdfURL as props
const PDFModal = (props) => {
    const { ......., pdfURL } = props;
    
    //...skipped code    
  
    return (
       <StyledDialog
            PaperComponent={PaperComponent}
       >
           //...skipped code
           <PDFViewer
              url={pdfURL}
           />
       </StyledDialog> 
    );
};

  1. PDFViewer updated because there is a new prop.
const PDFViewer = ({ url }) => {
   //...skipped code
   return (
       //...skipped code
       <Viewer
          fileUrl={url}
       />
   );
}

I agree what @LindaPaiste said, putting Toolbar maybe an option since it doesn't use the url props passed in. For the re-render problem, I suggest that useCallback can be used to wrap the whole PDFViewer component. Only update the component when the url has changed.

This link provide some insights on when to use useCallback which can be a reference.

const PDFViewer = useCallback(
    ({ url }) => {
        //...skipped code
        return (
            //...skipped code
            <Viewer
                fileUrl={url}
            />
        )
}, [url])
tcf01
  • 1,699
  • 1
  • 9
  • 24