Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React PDF viewer component rerenders constantly

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/[email protected]/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?

like image 288
Ludwig Avatar asked Jan 23 '17 09:01

Ludwig


People also ask

How do I stop multiple re rendering in React?

1. Memoization using useMemo() and UseCallback() Hooks. Memoization enables your code to re-render components only if there's a change in the props. With this technique, developers can avoid unnecessary renderings and reduce the computational load in applications.

Why does React render multiple times?

You can see in the console tab, that the render lifecycle got triggered more than once on both the app and greeting component. This is because the React app component got re-rendered after the state values were modified, and it also re-rendered its child components.

Does changing state re-render?

React components automatically re-render whenever there is a change in their state or props. A simple update of the state, from anywhere in the code, causes all the User Interface (UI) elements to be re-rendered automatically.


1 Answers

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])
like image 132
tcf01 Avatar answered Oct 05 '22 09:10

tcf01