Search code examples
reactjsmaterial-table

Update a single material-table row that is originally sourced from remote data


I have a React material-table. I want one of the columns to be my custom <DatePicker/> component, where the user can assign a scheduled date to the row.

The table currently gets data remotely.

When the <DatePicker/> component is used and a date is selected, I don't want the entire table to re-fetch data from the remote source. Instead I want my <DatePicker/> onChange handler to modify the current data (the row I'm setting the date on) in the table.

Is there a way to tap into material-tables data store and call a "setData" function so that I can update only the target row?

Normally, if the table's data wasn't from a remote source I'd just call my own "setData" function, but because I am taking advantage of material-tables built in remote data option and using filters I can't do this.


Solution

  • I wasn't able to find a clean way to achieve tapping into material-table's data and updating a row when the data comes from a remote source. However I was able to come up with a somewhat 'hacky' solution that works.

    To accomplish this I did the following:

    1. Created a tableRef to the material-table and stored this in my Redux store.
    2. Created local component state to store my previous query param history.
    3. Set the data prop on MaterialTable to a custom getData function, which returns a Promise, to either hit the backend api to get the table data (all the rows), or to modify the current table data and then return this new modified data. This function uses tableRef.current.dataManager.getRenderState().data to get the current MaterialTable data. Then returns a modified version of it (if required).
    4. Created a small Component (NextAction) that take 2 props - rowData from MaterialTable and tableRef from Redux store - to render the column holding my custom action button which will be clicked to modify the current table data. This component also has access to the tableRef of the material-table stored in the Redux store. This component (which is ultimately a button) takes advantage of the onQueryChange() function on the tableRef to manually force a call on the getData function.
    5. Set the render key in my column definition to return this component called NextAction.

    The idea is to have a column with a button. When the button is pressed that row's 'current_state' column is modified, and the table data is NOT re-rendered from the api.

    This works because when the button is clicked the tableRef.current.onQueryChange() function is invoked. Because the component's 'data' prop is set to a custom getData function, when tableRef.current.onQueryChange() is called, MaterialTable is told call the data function which we set on the MaterialTable component to our custom getData function.

    Within this getData function we then check to see if the new query params are different than the old query params (as the query params are also stored in local state). If they are the same, we check to see if the getData query param has a nextState key. This nextState key is only present when getData is invoked from the custom button. If it is, we use the tableRef to access the current table data, modify the row we want to, and then return the new tableData (still wrapped in a promise).

    Here is some sample code achieving the aforementioned:

    import React, { useContext } from 'react';
    import { connect } from 'react-redux';
    import axios from 'my/path/to/axios';
    
    /*
      Custom Button  rendered in a column on the MaterialTable
      Takes a tableRef prop from the Redux Store.
      Note the onClick function that is called.
    */
    const NextActionButton = connect(mapStateToPropsNextAction)(( props ) => {
      const tableRef = props.tableRef; // captured from redux store - tableRef from MaterialTable
      return (
        <Button
          disabled={false}
          variant="contained"
          color="secondary"
          size="large"
          onClick={(e) => {tableRef.current.onQueryChange({
            onNextAction: true,
            nextState: 'dispatched',
            row_id: props.rowData.tableData.id,
          })}}
          >
          Dispatch
        </Button>
      )
    })
    
    const MyComponent = ( props ) => {
    
      let tableRef = React.createRef()  // Create the tableRef
      props.setTableRef(tableRef)
    
      // Store the query history
      const [queryHist, setQueryHist] = React.useState({});
    
      // Custom function to return the table data either via api or from the old state with modifications
      const getData = (query, tableRef) => new Promise((resolve, reject) => {
        const queryParams = {
          filters: query.filters,
          orderBy: query.orderBy,
          orderDirection: query.orderDirection,
          page: query.page,
          pageSize: query.pageSize,
          search: query.search,
        }
    
        /*
        Here is the magic that allows us to update the current tableData state without hitting the api
    
        If the query is the same as before, then just return the same table without hitting api
        This occurs only when:
          1.) the user types the same filter
          2.) when the user wants to update a single row through the special update component in the update column
        */
        if (JSON.stringify(queryHist) === JSON.stringify(queryParams)) {
          // This is how we get MaterialTable's current table data. This is a list of objects, each object is a row.
          let newData = tableRef.current.dataManager.getRenderState().data; 
          if (query.onNextAction === true) {
            newData[query.row_id].current_state = query.nextState;
          }
          resolve({
            data: newData,
            page: tableRef.current.state.query.page,
            totalCount: tableRef.current.state.query.totalCount,
          })
        }
    
        setQueryHist(queryParams)  // Store query params in the local state for future comparison
    
        axios.get('/my_data_api', {
          params: queryParams
        }).then(response => {
            return resolve({
              data: response.data.data,
              page: response.data.page,
              totalCount: response.data.total_count,
            })
          }).catch(function (error) {
            console.log(error);
          })
      })
    
      const [columns] = React.useState([
        {
          title: "Record Id",
          field: "record_id",
        },
        {
          title: "Current State",
          field: "current_state",  // This is the field we will be updating with the NextActionButton
        },
        {
          title: "Next Action",
          field: "placeholder",
          filtering: false,
          render: (rowData) =>  <NextActionButton rowData={rowData}/> // Render the custom button component
        },
      ])
    
      return (
        <MaterialTable
          title="My Table Title"
          tableRef={tableRef}  // Assign the ref to MaterialTable
          data={
            (query) => getData(query, tableRef)
          }
          columns={columns}
          options={{
            filtering: true,
            debounceInterval: 600,
            paging: true,
            pageSize: 50,
            pageSizeOptions: [50, 100, 1000],
            emptyRowsWhenPaging: false,
            selection: true,
          }}
          .
          .
          .
    
        />
      )
    }
    
    // Redux stuff
    const mapDispatchToProps = dispatch => {
      return {
        setTableRef: (tableRef) => dispatch({type: 'SET_TABLE_REF', 'ref': tableRef})
        .
        .
        .
      };
    };
    
    export default connect(null, mapDispatchToProps)(Workorders);