gregnb / mui-datatables

Datatables for React using Material-UI
MIT License
2.71k stars 932 forks source link

How to prevent re-render after click on custom column buttons #822

Open ydjyos opened 5 years ago

ydjyos commented 5 years ago

Hello,

This is an awesome package that have been very helpful for me and a true time saver (so thanks ! ) but I have an issue with the table re-rendering on click on buttons.

I have a column with customBodyRender containing a button for downloading a file. On click on this button the state value "isLoading" is set to true in order to display a loading spinner. The table data is not at all updated so there is no need to re-render the table and lose all filters or sorting.

Is there an option to prevent the re-rendering ? or any workaround ?

Expected Behavior

The table should not re-render if the data is not changed.

Current Behavior

The table re-render on click on a button that does not update the table data.

Steps to Reproduce (for bugs)

  1. create a custom column with button having onclick function that updates a state other than the table data.
  2. select a filter or sort a column
  3. click on the button
  4. table is re-rendered having lost all filters, sorting (and probably pagination).

Your Environment

Tech Version
Material-UI 4.0.0
MUI-datatables 2.7.0
React 16.8.6
browser Chrome, IE
gabrielliwerant commented 5 years ago

Hello @ydjyos. It's difficult to tell without seeing your code, but you be having the same issue described here https://github.com/gregnb/mui-datatables/issues/807. I will mark as duplicate for now, but if you can demonstrate it's different, I'll reevaluate. Assuming it's the same problem, I give the solution at the bottom of the linked issue.

ydjyos commented 5 years ago

Hi, Actually, for me it is not the issue of saving the sorting options and the filters , it's more about preventing unnecessary renders of the table if the data is not updated.

So this is my code for the dashboard component containign the MUIDataTable (I tried to remove irrelevant code )

/**
 * @file
 * dashboard Component
 * @author
 * YDJ
 **/
import React from "react";
import { Redirect } from "react-router-dom";
import moment from "moment";
import API from "../utils/api";
import PropTypes from "prop-types";
import classNames from "classnames";
import MUIDataTable from "mui-datatables";
import Moment from "react-moment";
import "moment/locale/fr";
import { saveAs } from "file-saver";
/*Material components*/
import withStyles from "@material-ui/core/styles/withStyles";
import { MuiThemeProvider } from "@material-ui/core/styles";
import {
  Grid,
  Typography,
  Box,
  Tooltip,
  IconButton,
  Snackbar,
  Slide,
  CircularProgress
} from "@material-ui/core";
/*Material icons*/
import {
  Close,
  SaveAlt,
  AccessTime,
  Check,
  HighlightOff,
  ErrorOutline
} from "@material-ui/icons";

/* custom components*/
import ColoredLinearProgress from "../components/ColoredLinearProgress/ColoredLinearProgress.jsx";
import ArrowTooltip from "../components/ArrowTooltip/ArrowTooltip.jsx";

/*Style*/
import customStyle from "../assets/jss/hrsign-react/views/dashboardStyle.js";
import config from "../utils/config";

const apiErrorsListMessages = {
  EDIT_LOCK_ENVELOPE_LOCKED:
    "L'enveloppe est déjà en cours de modification. Réessayez ultérieurement.",
  INVALID_REQUEST_PARAMETER: "Echec, l'enveloppe est corrompue.",
  ENVELOPE_DOES_NOT_EXIST: "Echec, l'enveloppe est introuvable."
};

function TransitionUp(props) {
  return <Slide {...props} direction="up" />;
}

class Dashboard extends React.Component {
  _isMounted = false;
  constructor(props) {
    super(props);
    this.state = {
      isLoading: true,
      isExecuting: false,
      redirect: false,
      signRequests: [],
      snackbarOpen: false,
      snackbarMessage: ""
    };
  }

  componentDidMount() {
    this._isMounted = true;
    //set the logged in user in application state level
    this.props.getAuthUser(this.props.data);
    API.list(sessionStorage.getItem("accessToken"))
      .then(signReqList => {
        if (this._isMounted) {
          this.setState({
            isLoading: false,
            signRequests: this.prepareTableData(signReqList.data.signRequests),
          });
        }
      })
      .catch(error => {
        return error;
      });
  }

  componentWillUnmount() {
    this._isMounted = false;
  }

  handleSnackbarClose = () => {
    this.setState({ snackbarOpen: false });
  };

  /**
   *@function
   * process received data in array of JSON objects format to convert each item to an array of data
   *@param  String listOfSignRequests Array of JSON objects
   *@returns Array of array
   **/
  prepareTableData(listOfSignRequests) {
    let tableData = [];
    tableData = listOfSignRequests.map(row => {
      const statusFr = config.signReqStatusFr[row.status];
      return [
        row.signRequestId,
        statusFr,
        row.currentRecipient,
        row.lastUpdateDate,
        row.employee.fullName,
        row.documents[0].groupType,
        row.employee.companyLabel,
        row.employee.businessLine,
        null, //actions 
        row.creationDate,
        row.employee.email,
        row.employee.hrId,
        row.employee.service,
        row.voidReason,
        row.declineReason,
        row.declinedBy
      ];
    });
    return tableData;
  }

  /**
   *@function
   * redirect to login route
   *@param
   **/
  handleReAuth = () => {
    this.setState({
      isExecuting: true,
      redirect: true
    });
  };

  handleDownloadZip = envelopeData => {
    this.setState({
      isExecuting: true,
      snackbarMessage: "Téléchargement des documents en cours ...",
      snackbarOpen: true,
      Transition: TransitionUp
    });
    API.downloadZip(envelopeData[0], sessionStorage.getItem("accessToken"))
      .then(result => {
        if (this._isMounted) {
          var blob = new Blob([result.data], { type: "application/zip" });
          var fileName =
            envelopeData[4] +
            "_" +
            envelopeData[5] +
            ".zip"; /* filename = fullename_grouptype.zip*/
          saveAs(blob, fileName);

          this.setState({
            isExecuting: false,
            snackbarMessage: "Les documents ont bien été téléchargés.",
            snackbarOpen: true,
            Transition: TransitionUp
          });
        }
      })
      .catch(error => {
        if (error.data && error.data.message) {
          this.setState({
            snackbarMessage: [apiErrorsListMessages[error.data.message]],
            isExecuting: false,
            snackbarOpen: true,
            Transition: TransitionUp
          });
        } else {
          if (error.status === 401) {
            this.setState({
              snackbarOpen: true,
              Transition: TransitionUp,
              snackbarMessage: "Authentification en cours..."
            });
            this.handleReAuth();
          } else {
            this.setState({
              snackbarMessage: "Une erreur est survenue",
              isExecuting: false,
              snackbarOpen: true,
              Transition: TransitionUp
            });
          }
        }
      });
  };
  render() {
    const {
      signRequests,
      snackbarOpen,
      snackbarMessage,
      isLoading,
      isExecuting,
      redirect,,
    } = this.state;
    const { classes } = this.props;

    if (isLoading) {
      return <ColoredLinearProgress />;
    }
    if (redirect) {
      return (
        <Redirect
          to={{
            pathname: "/login",
            state: { redirectpath: window.location.pathname }
          }}
        />
      );
    }
    const statusIcons = {
      Erreur: <ErrorOutline className={classes.dangerIcon} />,
      Invalidé: <HighlightOff className={classes.dangerIcon} />,
      Refusé: <HighlightOff className={classes.dangerIcon} />,
      Complété: <Check className={classes.successIcon} />,
      "En cours": <AccessTime className={classes.infoIcon} />
    };

    const columns = [
      {
        name: "ID",
        options: {
          filter: false,
          sort: false,
          display: "excluded"
        }
      },
      {
        name: "Statut",
        options: {
          filter: true,
          sort: true,
          customBodyRender: (value, tableMeta, updateValue) => (
            <Tooltip title={value} placement="top">
              {statusIcons[value]}
            </Tooltip>
          )
        }
      },
      {
        name: "Destinataire actuel",
        options: {
          filter: false,
          sort: true,
          customBodyRender: (value, tableMeta, updateValue) =>
            (tableMeta.rowData && tableMeta.rowData[1]) === "Invalidé" ? (
              <ArrowTooltip
                title={
                  <React.Fragment>
                    <Typography variant="subtitle2">
                      {"Raison d'invalidation"}
                    </Typography>
                    <Typography variant="body2">
                      {tableMeta.rowData[13]}
                    </Typography>
                  </React.Fragment>
                }
                placement="top"
              >
                <Typography color={"error"}>{value}</Typography>
              </ArrowTooltip>
            ) : (tableMeta.rowData && tableMeta.rowData[1]) === "Refusé" ? (
              <ArrowTooltip
                title={
                  <React.Fragment>
                    <Typography component="div" variant="body2">
                      <Box display="inline" fontWeight="Bold" m={1}>
                        {tableMeta.rowData[15]}
                      </Box>
                      <Box display="inline">a refusé la signature.</Box>
                    </Typography>
                    <Typography component="div" variant="body2">
                      <Box display="inline" fontWeight="Bold" m={1}>
                        Raison:
                      </Box>
                      <Box display="inline">{tableMeta.rowData[14]}</Box>
                    </Typography>
                  </React.Fragment>
                }
                placement="top"
              >
                <Typography color={"error"}>{value}</Typography>
              </ArrowTooltip>
            ) : (
              <Typography>{value}</Typography>
            )
        }
      },
      {
        name: "Dernière mise à jour",
        options: {
          filter: false,
          sort: true,
          customBodyRender: (value, tableMeta, updateValue) => (
            <Moment format="ll">{value}</Moment>
          )
        }
      },
      {
        name: "Salarié",
        options: {
          filter: false,
          sort: true,
          customBodyRender: (value, tableMeta, updateValue) => (
            <ArrowTooltip
              title={
                <React.Fragment>
                  <Typography component="div" variant="body2">
                    <Box
                      display="inline"
                      fontStyle="italic"
                      fontWeight="Bold"
                      m={1}
                    >
                      Email:
                    </Box>
                    <Box display="inline">{tableMeta.rowData[10]} </Box>
                  </Typography>
                  <Typography component="div" variant="body2">
                    <Box
                      display="inline"
                      fontStyle="italic"
                      fontWeight="Bold"
                      m={1}
                    >
                      Date de création:
                    </Box>
                    <Box display="inline">
                      <Moment format="ll">{tableMeta.rowData[9]}</Moment>
                    </Box>
                  </Typography>
                  <Typography component="div" variant="body2">
                    <Box
                      display="inline"
                      fontStyle="italic"
                      fontWeight="Bold"
                      m={1}
                    >
                      Matricule TGRH:
                    </Box>
                    <Box display="inline"> {tableMeta.rowData[11]}</Box>
                  </Typography>
                  <Typography component="div" variant="body2">
                    <Box
                      display="inline"
                      fontStyle="italic"
                      fontWeight="Bold"
                      m={1}
                    >
                      Service:
                    </Box>
                    <Box display="inline"> {tableMeta.rowData[12]}</Box>
                  </Typography>
                </React.Fragment>
              }
              placement="top"
            >
              <Typography>{value}</Typography>
            </ArrowTooltip>
          )
        }
      },
      {
        name: "Type de document",
        options: {
          filter: true,
          sort: true
        }
      },
      {
        name: "Société",
        options: {
          filter: true,
          sort: true
        }
      },
      {
        name: "Ligne métier",
        options: {
          filter: true,
          sort: true
        }
      },
      {
        name: "Actions", //8
        options: {
          filter: false,
          sort: false,
          customBodyRender: (value, tableMeta, updateValue) => (
            <React.Fragment>
              <Tooltip title="Télécharger" placement="top">
                <IconButton
                  component="div"
                  disabled={
                    tableMeta.rowData && tableMeta.rowData[1] !== "Complété"
                      ? true
                      : false
                  }
                  aria-label="Download"
                  className={classNames(classes.tableActionButton, {
                    [classes.noPointerEvent]: isExecuting
                  })}
                  onClick={() => this.handleDownloadZip(tableMeta.rowData)}
                >
                  <SaveAlt
                    className={
                      tableMeta.rowData && tableMeta.rowData[1] !== "Complété"
                        ? classes.tableActionButtonIcon
                        : classes.tableActionButtonIcon +
                          " " +
                          classes.successIcon
                    }
                  />
                </IconButton>
              </Tooltip>
            </React.Fragment>
          )
        }
      },
      {
        name: "Date de création",
        options: {
          filter: false,
          sort: false,
          display: "excluded"
        }
      },
      {
        name: "Email salarié",
        options: {
          filter: false,
          sort: false,
          display: "excluded"
        }
      },
      {
        name: "Matricule TGRH",
        options: {
          filter: false,
          sort: false,
          display: "excluded"
        }
      },
      {
        name: "Service",
        options: {
          filter: false,
          sort: false,
          display: "excluded"
        }
      },
      {
        name: "Raison invalidation",
        options: {
          filter: false,
          sort: false,
          display: "excluded"
        }
      },
      {
        name: "Raison refus",
        options: {
          filter: false,
          sort: false,
          display: "excluded"
        }
      },
      {
        name: "Refusé par",
        options: {
          filter: false,
          sort: false,
          display: "excluded"
        }
      }
    ];
    const tableOptions = {
      selectableRows: "none",
      print: false,
      download: false,
      textLabels: {
        body: {
          noMatch: "Aucune donnée ne correspond à votre recherche",
          toolTip: "Trier"
        },
        pagination: {
          next: "Page suivante",
          previous: "Page précédente",
          rowsPerPage: "Lignes par page:",
          displayRows: "sur"
        },
        toolbar: {
          search: "Rechercher",
          viewColumns: "Afficher colonnes",
          filterTable: "Filtrer"
        },
        filter: {
          all: "Tout",
          title: "FILTRER",
          reset: "Réinitialiser"
        },
        viewColumns: {
          title: "Afficher colonnes",
          titleAria: "Afficher/Cacher des colonnes"
        }
      }
    };
    return (
      <div className="Dashboard">
        <MuiThemeProvider theme={customStyle.responsiveTheme}>
          <Grid container spacing={8}>
              <Grid item className={classes.gridItem} xs={12}>
              <MuiThemeProvider theme={customStyle.getMuiDataTableTheme()}>
                {isExecuting && (
                  <div className={classes.loadingOverlay}>
                    <CircularProgress
                      variant="indeterminate"
                      color="secondary"
                    />
                  </div>
                )}
                <MUIDataTable
                  title={"Suivi des signatures électroniques"}
                  data={signRequests}
                  columns={columns}
                  options={tableOptions}
                />
              </MuiThemeProvider>
            </Grid>
          </Grid>
          <React.Fragment>
            <Snackbar
              anchorOrigin={{
                vertical: "bottom",
                horizontal: "center"
              }}
              open={snackbarOpen}
              autoHideDuration={3000}
              onClose={this.handleSnackbarClose}
              TransitionComponent={this.state.Transition}
              ContentProps={{
                "aria-describedby": "message-id"
              }}
              message={<span id="message-id">{snackbarMessage}</span>}
              className={classes.marginSnackbar}
            />
          </React.Fragment>
        </MuiThemeProvider>
      </div>
    );
  }
}

Dashboard.propTypes = {
  classes: PropTypes.object.isRequired
};

export default withStyles(customStyle.dashboardStyle)(Dashboard);

I've implemented a new version of this code where I encapsulate the MUIDataTable in another component where I can control the re-rendering with shouldComponentUpdate This is the code in customDataTable.jsx :

import React from "react";
import MUIDataTable from "mui-datatables";

class CustomDataTable extends React.Component {
  constructor(props) {
    super(props);
  }
  shouldComponentUpdate(nextProps, nextState) {
    return this.props.data !== nextProps.data ? true : false;
  }

  render() {
    return (
      <MUIDataTable
        title={this.props.title}
        data={this.props.data}
        columns={this.props.columns}
        options={this.props.options}
      />
    );
  }
}

export default CustomDataTable;

and the only change in my dashboard component is : the import :

import CustomDataTable from "../components/Table/customDataTable.jsx";

and using CustomDataTable instead of MUIDataTable component :

<CustomDataTable
                  title={"Suivi des signatures électroniques"}
                  data={signRequests}
                  columns={columns}
                  options={tableOptions}
                />

I've found that this fixes my issue and when I click on download action button the table is not re-rendered. And for now I don't see any issues using it.

Any thoughts on this solution are welcomed.

patorjk commented 5 years ago

@ydjyos Looking at your code, it looks like toggling the value of isLoading would cause the datatable to be unmounted from the DOM. It's not clear how your workaround works. I built a small codesandbox that mimics the idea:

https://codesandbox.io/s/muidatatables-custom-toolbar-dv8xb

But as you can see, the filters are still reset.

To avoid the re-rendering issue, you could keep the datatable mounted, and instead have a component render over the table when it's loading (like a lightbox or something).

gabrielliwerant commented 5 years ago

Good call @patorjk.

@ydjyos I would use one return statement for your component in the render function, only, and use if clauses to determine what displays instead.

ydjyos commented 5 years ago

Thanks for your suggestions. I'm not sure that this issue is about isLoading or isExecuting toggIing. I will try with only one return statement and update the post.