简体   繁体   中英

Trigger re-render of subcomponent (react-table) using hooks

I'm still new to React, and functional programming, and Javascript, and JSX, so go easy if this is a stupid question.

I'm modifying one of the example material-ui tables from react-table v7. The original code can be found here . The example is completely functional and is using React Hooks as opposed to classes, as do all of the components of the template I'm using (shout out to creative-tim.com!)

My parent function (representative of a page in my dashboard application), for instance Users.js or Stations.js fetches data from a backend api inside a useEffect hook. That data is then passed as a prop to my subcomponent ReactTables.js

For some reason ReactTables.js does not receive changes to the "data" prop after the parent page's useEffect finishes. However, once I modify the data from a subcomponent of ReactTables (in this case AddAlarmDialog.js) then the table re-renders and all of my data suddenly appears.

How can I trigger the re-render of my subcomponent when data is returned from the parent component's useEffect? I noticed that in older versions of React there was a lifecycle function called componentWillReceiveProps(). Is this the behavior I need to emulate here?

Example Parent Component (Alarms.js):

import React, { useEffect, useState } from "react";
// @material-ui/core components

// components and whatnot
import GridContainer from "components/Grid/GridContainer.js";
import GridItem from "components/Grid/GridItem.js";

import ReactTables from "../../components/Table/ReactTables";

import { server } from "../../variables/sitevars.js";

export default function Alarms() {
  const [columns] = useState([
    {
      Header: "Alarm Name",
      accessor: "aName"
    },
    {
      Header: "Location",
      accessor: "aLocation"
    },
    {
      Header: "Time",
      accessor: "aTime"
    },
    {
      Header: "Acknowledged",
      accessor: "aAcked"
    },
    {
      Header: "Active",
      accessor: "aActive"
    }
  ]);

  const [data, setData] = useState([]);
  const [tableType] = useState("");
  const [tableLabel] = useState("Alarms");

  useEffect(() => {
    async function fetchData() {
      const url = `${server}/admin/alarms/data`;
      const response = await fetch(url);
      var parsedJSON = JSON.parse(await response.json());

      var tableElement = [];
      parsedJSON.events.forEach(function(alarm) {
        tableElement = [];
        parsedJSON.tags.forEach(function(tag) {
          if (alarm.TagID === tag.IDX) {
            tableElement.aName = tag.Name;
          }
        });
        tableElement.aTime = alarm.AlarmRcvdTime;
        parsedJSON.sites.forEach(function(site) {
          if (site.IDX === alarm.SiteID) {
            tableElement.aLocation = site.Name;
          }
        });
        if (alarm.Active) {
          tableElement.aActive = true;
        } else {
          tableElement.aActive = false;
        }
        if (!alarm.AckedBy && !alarm.AckedTime) {
          tableElement.aAcked = false;
        } else {
          tableElement.aAcked = true;
        }
        //const newData = data.concat([tableElement]);
        //setData(newData);
        data.push(tableElement);
      });
    }
    fetchData().then(function() {
      setData(data);
    });
  }, [data]);

  return (
    <div>
      <GridContainer>
        <GridItem xs={12} sm={12} md={12} lg={12}>
          <ReactTables
            data={data}
            columns={columns}
            tableType={tableType}
            tableLabel={tableLabel}
          ></ReactTables>
        </GridItem>
      </GridContainer>
    </div>
  );
}

Universal Table Subcomponent (ReactTables.js):

import React, { useState } from "react";

// @material-ui/core components
import { makeStyles } from "@material-ui/core/styles";
// @material-ui/icons
import Assignment from "@material-ui/icons/Assignment";

// core components
import GridContainer from "components/Grid/GridContainer.js";
import GridItem from "components/Grid/GridItem.js";
import Card from "components/Card/Card.js";
import CardBody from "components/Card/CardBody.js";
import CardIcon from "components/Card/CardIcon.js";
import CardHeader from "components/Card/CardHeader.js";

import { cardTitle } from "assets/jss/material-dashboard-pro-react.js";
import PropTypes from "prop-types";
import EnhancedTable from "./subcomponents/EnhancedTable";

const styles = {
  cardIconTitle: {
    ...cardTitle,
    marginTop: "15px",
    marginBottom: "0px"
  }
};

const useStyles = makeStyles(styles);

export default function ReactTables(props) {
  const [data, setData] = useState(props.data);
  const [columns] = useState(props.columns);
  const [tableType] = useState(props.tableType);
  const [skipPageReset, setSkipPageReset] = useState(false)

  const updateMyData = (rowIndex, columnId, value) => {
    // We also turn on the flag to not reset the page
    setData(old =>
      old.map((row, index) => {
        if (index === rowIndex) {
          return {
            ...old[rowIndex],
            [columnId]: value
          };
        }
        return row;
      })
    );
  };

  const classes = useStyles();
  return (
    <GridContainer>
      <GridItem xs={12}>
        <Card>
          <CardHeader color="primary" icon>
            <CardIcon color="primary">
              <Assignment />
            </CardIcon>
            <h4 className={classes.cardIconTitle}>{props.tableLabel}</h4>
          </CardHeader>
          <CardBody>
            <EnhancedTable
              data={data}
              columns={columns}
              tableType={tableType}
              setData={setData}
              updateMyData={updateMyData}
              skipPageReset={skipPageReset}
              filterable
              defaultPageSize={10}
              showPaginationTop
              useGlobalFilter
              showPaginationBottom={false}
              className="-striped -highlight"
            />
          </CardBody>
        </Card>
      </GridItem>
    </GridContainer>
  );
}

ReactTables.propTypes = {
  columns: PropTypes.array.isRequired,
  data: PropTypes.array.isRequired,
  tableType: PropTypes.string.isRequired,
  tableLabel: PropTypes.string.isRequired,
  updateMyData: PropTypes.func,
  setData: PropTypes.func,
  skipPageReset: PropTypes.bool
};

**For the record: if you notice superfluous code in the useEffect it's because I was messing around and trying to see if I could trigger a re-render.

I dont know exactly how the reactTable is handling its rendering, but if its a pure functional component, then the props you pass to it need to change before it will re-evaluate them. When checking if props have changed, react will just do a simple === comparison, which means that if your props are objects whos properties are being modified, then it will still evaluate as the same object. To solve this, you need to treat all props as immutable

In your example, you are pushing to the data array, and then calling setData(data) which means that you are passing the same instance of the array. When react compares the previous version of data, to the new version that you are setting in the call to setDate , it will think data hasnt changed because it is the same reference.

To solve this, you can just make a new array from the old array by spreading the existing array into a new one. So, instead of doing

data.push(tableElement);

You should do

const newInstance = [...data, tableElement];

Your code will need some tweaking because it looks like you are adding in lots of tableElements. But the short version of the lesson here is that you should never try and mutate your props. Always make a new instance

EDIT: So, after looking again, I think the problem is the way you are using the default param in the useState hook. It looks like you are expecting that to set the state from any prop changes, but in reality, that param is simply the default value that you will put in the component when it is first created. Changing the incoming data prop doesn't alter your state in any way.

If you want to update state in response to changes in props, you will need to use the useEffect hook, and set the prop in question as a dependancy.

But personally, I would try and not have what is essentially the same data duplicated in state in two places. I think the best bet would be to store your data in your alarm component, and add a dataChanged callback or something which will take your new data prop, and pass it back up to alarm via a parameter in the callback

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM