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.
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:
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).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.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);