4

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.

TheFunk
  • 981
  • 11
  • 39

1 Answers1

5

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

James Considine
  • 323
  • 2
  • 13
  • I had read about this check on props in the hooks documentation, hence the two commented lines above data.push() where I tried to create new instances of the data array, but even when I create a new instance of the data array the table still does not want to populate until after I add a new element with the AddAlarmsDialog.js subcomponent.:( – TheFunk Mar 30 '20 at 20:37
  • I really appreciate the help here. I think you are correct, but I'm having a tough time understanding the flow of operations and putting that sort of dataChanged property into to practice. I had originally tried to create a boolean like that in the Alarms.js file. I called it something like dataPopulated and then used a terinary operator at the bottom of the file to only render the ReactTables component after the useEffect in the Alarms.js was finished running. This didn't work either. Instead I still just had an empty table to start. – TheFunk Apr 01 '20 at 14:56
  • I think that creating copies of data is going to cause slowness issues, particularly when dealing with a big dataset like this, so if I can implement a callback of some sort to pass the data back up to Alarms.js that would be best I think. That was actually one of my concerns with React. I'm used to a language where I can pass things around by reference as much as I want, that doesn't exist here. – TheFunk Apr 01 '20 at 14:58
  • 1
    React are going hard down the functional route. To write the most performant react components, you would need to make your components pure (a pure function is a function that will always produce the same output given the same inputs. e.g. an add function should always produce 4 when given 2 and 2 as its params). Given this purity, immutability becomes an absolutely crucial part of react going forward, and you can see this with the way they have implemented things like the `useMemo` and `useCallback` hooks. It does take some getting used to, but if you embrace it, it is pretty fun to write – James Considine Apr 01 '20 at 17:34
  • 2
    Hey! Good news! You were right! I removed the copies of the stateful components from the child component and the issue was resolved! In order to prevent an infinite loop, I had to change dependencies at the top level to an empty array in the useEffect() hook and I hear that's bad BUT I'll look into fixing that later. Problem solved! Thank you very much! – TheFunk Apr 03 '20 at 17:26