React.useMemo does not update the data - reactjs

I am new to hooks. So this might be easy yet I have no idea how to solve:
I have a function like this which takes two arrays columns and data . and those data should be memoized or else it does not work. (recommended by react-table guys)
function ReactTable(props) {
const columns = React.useMemo(() => props.columns, [])
const data = React.useMemo(() => props.data, [])
return <Table columns={columns} data={data} />
}
this works fine but when the props change (say an item is added or removed from data array), the React.useMemo won't send the updated data to the Table component. How can I resolve this :(

This is exactly what the dependency array in hooks is for. You can define variables that 'trigger' the change on hooks. In your case this would mean that you would need to change your code to the following:
function ReactTable(props) {
const columns = React.useMemo(() => props.columns, [props.columns]);
const data = React.useMemo(() => props.data, [props.data]);
return <Table columns={columns} data={data} />
}
This means, whenever props.columns changes, the columns variable is updated and the same for props.data and data.

Mind you above answer from user Linschlager might not work if you're using react-table's sorting hook useSortBy. Indeed, the authors final solution did not involve react.useMemo.
To me it worked out anyways. My columns- and row-data came from a query-data object that I had to resolve to fit the specific way react-table does it.
It looked something like this:
function ReportTable({ queryData }) {
... other data such as {selectedPerLevel} ...
/* COLUMNS */
let firstColumn = {
Header: ' ',
columns: [
{
Header: selectedPerLevel.name,
accessor: 'perLevel',
},
],
};
let otherColumns = [];
queryData.weeks.forEach((week) => {
let otherColumn = {
Header: week,
Footer: ' ',
center: true,
columns: [
{
Header: 'Ratio',
accessor: `ratio${week}`,
},
{
Header: 'Count',
accessor: 'count' + week,
},
],
};
otherColumns = [...otherColumns, otherColumn];
});
/* ROWS */
let listOfRows = queryData.units.map((unit) => {
let row = {};
// for each column
unit.items.forEach(({ week, ratio, count}) => {
row = {
...row,
['ratio' + week]: ratio,
['count' + week]: count,
};
});
// add the first column-data to the row
row = { ...row, perLevel: unit.name, id: unit.id };
return row;
});
const data = React.useMemo(() => listOfRows, [queryData]);
const columns = React.useMemo(() => [{ ...firstColumn }, ...otherColumns], [queryData]);
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = useTable({data, columns}, useSortBy);
return <Table ....
I don't know if this is to anyones help but it all worked out fine for me with above solution.

Related

Set multiple filters in react-table using setFilter

Is it possible to filter for multiple values using setFilter? This filters any instance of '31' in the age column:
useEffect(() => {
setFilter("age", "31");
});
I have tried adding an array but it doesn't work:
useEffect(() => {
setFilter("age", ["31", "32"]);
});
Yes. You can pass any value including multiple values (array) into setFilter setter.
As writen here:
setFilter: Function(columnId, filterValue) => void
filterValue: any
But, we have to make sure that:
The setFilter setter is retrieved from the table instance by the using of useTable and useFilter hooks:
const {
...
setFilter,
...
} = useTable(
{
columns,
data,
},
useFilter
);
The custom filter process the filterValue correctly so it will return the intended result:
function multiSelectFilter(rows, columnIds, filterValue) {
// beware of "31".includes(1) and ["31", "32"].includes(1)
// this method will return a different value if you passed in a different value data type.
return filterValue.length === 0
? rows
: rows.filter((row) => filterValue.includes(row.original[columnIds]));
}
The custom filter is attached into the columns object together with its id:
columns: [
{
Header: "Age",
accessor: "age",
id: "age",
filter: multiSelectFilter // <--- put it here
},
]
Then you can use the filter at your table as follows:
function Table({ columns, data, filteredAges }) {
const {
...
setFilter,
...
} = useTable(
{
columns,
data,
},
useFilter
);
...
useEffect(() => {
if (filteredAges) {
setFilter("age", filteredAges);
}
}, [filteredAges, setFilter]);
...
return (
...
)
}
Here's the example:

dynamic antd table title - react

I am using antd table here, I successfully populated one of the title vertically and I wanna populate the other one horizontally, refer to the images for better clarification thanks
here is my table column:-
const columns = [
{
title: 'Days',
dataIndex: 'date',
defaultSorter: 'ascend',
key: 'title',
sorter: (a, b) => a.date.localeCompare(b.date),
sortDirections: ['descend', 'ascend'],
render: (date) => getDayName(new Date(date)),
},
{
title: 'period',
dataIndex: 'period',
},
}
Datasource or data from api
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [query, setQuery] = useState('');
const fetchData = async () => {
setLoading(true);
const { data } = await getTimetable();
setData(data);
setLoading(false);
};
useEffect(() => {
fetchData();
}, []);
rendering table data:-
<TableContainer columns={columns} rowKey={(record) => record.login.uuid} dataSource={data} />
What I want to achieve :point_down:
what I have right now:-
In antd Table, columns need to passed as below format.
const columns = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
},
// .... rest of the cols
];
So to create dynamic columns from the response data you get here, 1st you need to create the array of objects (cols) as the above format that need to pass for <Table columns={formattedColsfromResponseData}.
So in your case you need to create columns[] like below format.
let cols = [
{
title: "Days",
dataIndex: "date",
defaultSorter: "ascend",
key: "title",
sorter: (a, b) => a.date.localeCompare(b.date),
sortDirections: ["descend", "ascend"]
render: (date) => getDayName(new Date(date)),
},
{
title: "period 1"
key: "period 1"
render: (row) => row.section + " - " + row.subject; // render function for customized data.
},
{
title: "period 2"
key: "period 2"
render: (row) => row.section + " - " + row.subject; // render function for customized data
},
// .... rest of the 'Period n' cols in the response.
];
With the below method you can create cols as required format by passing the response data.
This method works assuming response data has only unique 'Periods'.
const generateColumns = (data) => {
let cols = [];
const days = {
title: "Days",
dataIndex: "date",
defaultSorter: "ascend",
key: "title",
sorter: (a, b) => a.date.localeCompare(b.date),
sortDirections: ["descend", "ascend"]
render: (date) => getDayName(new Date(date)),
};
cols.push(days); // add 'Days' obj to Columns.
// for render: property in cols need to return a function. Here creates that.
const generateRender = (row) => {
console.log("gen row----", row);
return (row) => row.section + " - " + row.subject;
};
// create the col objects for each 'Period'. This method works assuming response data has only unique 'Periods'.
data.map((row) => {
let period = {}; // for create 'Period' obj for cols.
period["title"] = row.period;
period["key"] = row.period;
period["render"] = generateRender(row); // only need this if you render customized data.
cols.push(period); // add Current Period obj to Columns.
});
return cols;
};
Now you can pass this method to columns prop in Table with response data which returns the dynamic cols.
const App = () => (
<div>
<Table columns={generateColumns(data)} dataSource={data} />
</div>
);
Check this full demo antd-dynamic-cols-example code.

Why the table data is not updated immediately after performing an action for the table

I have a table that has an action to delete..like this:
const deleteRow = (row) => {
let indexOfDeleted = -1;
let data = tableData;
data.forEach((item, index) => {
if (item.instrumentId === row.instrumentId) {
indexOfDeleted = index;
}
})
data.splice(indexOfDeleted, 1);
setTableData(data)
};
The data is deleted but I have to refresh it so that it is not displayed in the table.It does not seem to be rerender. What should I do?
for table:
const schema = {
columns: [
{
field: "persianCode",
title: "title",
},
],
operations: [
{
title: "delete",
icon: (
<DeleteIcon
className={clsx(classes.operationsIcon, classes.deleteIcon)}
/>
),
action: (row) => deleteRow(row),
tooltipColor: theme.palette.color.red,
}
],
};
You are mutating the state variable, in your deleteRow function. You should update the state with a copied array:
const deleteRow = (row) => {
setTableData(table => table.filter(data => data.instrumentId !== row.instrumentId))
};
Instead of finding the index and splicing it, you can just use the filter function. Since it returns a new array, we don't worry about mutating the state variable!
you will have to use Spread operator to reflect changes in react dom..
const deleteRow = (row) => {
let indexOfDeleted = -1;
let data = tableData;
data.forEach((item, index) => {
if (item.instrumentId === row.instrumentId) {
indexOfDeleted = index;
}
})
data.splice(indexOfDeleted, 1);
setTableData([...data]) /// like this
};

In react-table with React Hooks, how to make a get request inside Cell?

I have been working with React-table for a couple of days now, my first time using it, and I have ran into some issues I can't quite seem to resolve. I am trying to build a table where I can show data from two API get requests at the same time, and since I don't know if there is a way to connect the two requests data into one object, and I wouldn't know how to do it, I was trying to access some of the data with get requests inside the react-table Column Cell itself.
My case being: I have two objects, Contacts and Institutions, contacts have in their data the institution ID as parameter, and I need to show in the table both the contact information and some information of the institution that is linked to it, getting it from the institution ID that is present in the contact data.
Here is one example of contact:
{
"contact_id": "34378a25-fe8c-4c64-bd35-59eab3f30863",
"institution_id": "ae1d0fe8-cce1-40ef-87d7-729dfbe9716d",
"name": "Contato 2",
"role": "Cargo 1",
"phone_numbers": [],
"emails": [],
"createdAt": "2021-03-09T20:40:26.6863764Z",
"updatedAt": "2021-03-09T20:40:26.686376448Z",
"deleted": false
}
And here is the institution data:
{
"institution_id": "ae1d0fe8-cce1-40ef-87d7-729dfbe9716d",
"name": "Matheus Salles Blanco",
"socialReason": "teste",
"cnpj": "99999999999999",
"abbreviation": "Matheus",
"website": "teste.com",
}
This is the code that is being implemented, reduced to only the parts that matter and that is working, but only showing the info that is being fetched from the contact object:
const Contacts = ({ match }) => {
const [data, setData] = useState([]);
const [institution, setInstitution] = useState();
const dataRecoil = useRecoilValue(contactData);
const handleContact = useCallback(async () => {
const response = dataRecoil.data;
if (response) {
setData(response.filter((contact) => !contact.deleted));
}
}, [setData, dataRecoil]);
useEffect(() => {
handleContact();
}, [handleContact]);
const columns = useMemo(
() => [
{
Header: 'Nome',
accessor: 'name',
},
{
Header: 'Sigla',
accessor: 'abbreviation',
},
{
Header: 'Nome Fantasia',
accessor: 'institution_id',
},
],
[editToggle, handleDelete],
);
return (
<>
<Table columns={columns} data={data} />
</>
);
};
And a print of it:
And here is what I have been trying to do:
const Contacts = ({ match }) => {
const [data, setData] = useState([]);
const [institution, setInstitution] = useState();
const dataRecoil = useRecoilValue(contactData);
const handleContact = useCallback(async () => {
const response = dataRecoil.data;
if (response) {
setData(response.filter((contact) => !contact.deleted));
}
}, [setData, dataRecoil]);
useEffect(() => {
handleContact();
}, [handleContact]);
const columns = useMemo(
() => [
{
Header: 'Nome',
accessor: 'name',
},
{
Header: 'Sigla',
accessor: 'abbreviation',
},
{
Header: 'Nome Fantasia',
accessor: 'institution_id',
Cell: async ({ cell }) => {
const response = await getInstitutionById(cell.row.values.institution_id);
const result = [response.data];
const inst = result.map((inst) => {return inst.name});
const institution_name = inst[0];
console.log(institution_name);
return institution_name;
},
},
],
[editToggle, handleDelete],
);
return (
<>
<Table columns={columns} data={data} />
</>
);
};
Which works at the level of fetching the right data, but does not render the page and shows errors:
The error
The right data being shown in the console.log
The expected output would be to show those names on the console.log on place of that long ID of the first picture.
So, is it possible to do what I am trying to do? And if so, what might am I be doing wrong?
I believe the issue is that you are providing an async function for your cell, which will return a Promise, not the institution name as you are expecting.
A potential solution is to instead create a custom Cell component that uses state to store the institution name. I have provided an example below, which was guided by this example, however I have not tested the code at all, so use it as more of a guide.
const MyCell = ({ cell }) => {
const [institutionName, setInstitutionName] = useState('fetching...')
useEffect(() => {
const getInstitutionName = async (id) => {
const response = await getInstitutionById(id);
const result = [response.data];
const inst = result.map((inst) => {return inst.name});
const institution_name = inst[0];
console.log(institution_name);
setInstitutionName(institution_name)
}
getInstitutionName(cell.row.values.institution_id)
}
return institutionName
}
const Contacts = ({ match }) => {
const [data, setData] = useState([]);
const [institution, setInstitution] = useState();
const dataRecoil = useRecoilValue(contactData);
const handleContact = useCallback(async () => {
const response = dataRecoil.data;
if (response) {
setData(response.filter((contact) => !contact.deleted));
}
}, [setData, dataRecoil]);
useEffect(() => {
handleContact();
}, [handleContact]);
const columns = useMemo(
() => [
{
Header: 'Nome',
accessor: 'name',
},
{
Header: 'Sigla',
accessor: 'abbreviation',
},
{
Header: 'Nome Fantasia',
accessor: 'institution_id',
Cell: MyCell
},
],
[editToggle, handleDelete],
);
return (
<>
<Table columns={columns} data={data} />
</>
);
};

React Material-Table editing from props using hooks

I am building an application that will request data from an API and display it in an editable table, where the user can edit and update the data base. I am using React with material-ui and material-table.
I will initialize the data in the state of the parent component, and pass it as props to the child component that renders the table. For test purposes, I initialize the data in the state to simulate later implementation of props. The table renders correctly, but when I edit, the values don't change.
export default function Table(props){
const [gridData, setGridData] = useState({
data: [
{ param: "Admin", val: "0.03" },
{ param: "Margin", val: "0.4" },
{ param: "Price", val: "5080" },
],
resolve: () => {}
});
useEffect(() => {
gridData.resolve();
}, [gridData]);
const onRowUpdate = (newData, oldData) =>
new Promise((resolve, reject) => {
const { data } = gridData;
const index = data.indexOf(oldData);
data[index] = newData;
setGridData({ ...gridData, data, resolve });
});
const { data } = gridData;
return (
<div>
<MaterialTable
columns={props.col}
data={data}
editable={{
isEditable: rowData => true,
isDeletable: rowData => true,
onRowUpdate: onRowUpdate
}}
/>
</div>
);
}
Now, I found that the table works properly when I replace the columns={props.col} line with this:
columns={[
{ title: 'Parameters', field: 'param', editable: 'never' },
{ title: 'Value', field: 'val', editable: 'onUpdate' }
]}
So it appears that my problem is with the columns and not the data.
Any help would be greatly appreciated!
NOTE:
the code is based on this response from github: https://github.com/mbrn/material-table/issues/1325
EDIT:
The columns are passed from the parent component like this:
const comonscol = [
{ title: 'Parameters', field: 'param', editable: 'never' },
{ title: 'Value', field: 'val', editable: 'onUpdate' }
];
export default function ParamsSection(props) {
...
return (
<div>
...
<Table col={comonscol} data={dummy2} />
...
</div>
);
}
I'm not quite sure about what causing this issue but it seems that MaterialTable component doesn't trigger a re-render when columns data passed as a porps.
Here is how I fixed it:
First Approach:
Create a new state for columns and trigger re-render by updating the columns via useEffect:
const [gridData, setGridData] = useState(props.data);
const [columns, setcolumns] = useState(props.col);
useEffect(() => {
gridData.resolve();
// update columns from props
setcolumns(props.col);
}, [gridData, props.col]);
...
const onRowUpdate = (newData, oldData) =>
new Promise((resolve, reject) => {
// Reset the columns will trigger re-render as the state has changed
// then it will update by useEffect
setcolumns([]);
const { data } = gridData;
const updatedAt = new Date();
const index = data.indexOf(oldData);
data[index] = newData;
setGridData({ ...gridData, data, resolve, updatedAt });
});
codeSandbox Example.
Second Approach:
Merge data, columns into a state of object and make a copy of props data then use that copy. (I've changed the date structure a bit for testing)
// Parent
const data = [
{ param: "Admin", val: "0.03" },
{ param: "Margin", val: "0.4" },
{ param: "Price", val: "5080" }
];
const comonscol = [
{ title: "Parameters", field: "param" },
{ title: "Value", field: "val" }
];
...
<Table col={comonscol} data={data} />
// Table.js
const [gridData, setGridData] = useState({
data: props.data,
columns: props.col,
resolve: () => {},
updatedAt: new Date()
});
const onRowUpdate = (newData, oldData) =>
new Promise((resolve, reject) => {
// Copy current state data to a new array
const data = [...gridData.data];
// Get edited row index
const index = data.indexOf(oldData);
// replace old row
data[index] = newData;
// update state with the new array
const updatedAt = new Date();
setGridData({ ...gridData, data, updatedAt, resolve });
});
codeSandbox Example.
Note: onRowUpdate here as an example, same goes for onRowAdd, onRowDelete

Resources