I'm trying to update a react state that holds nested values. I want to update data that is 3 levels deep.
Here is the state that holds the data:
const [companies, setCompanies] = useState(companies)
Here is the data for the first company (the companies array holds many companies):
const companies = [
{
companyId: 100,
transactions: [
{
id: "10421A",
amount: "850",
}
{
id: "1893B",
amount: "357",
}
}
]
Here is the code for the table component:
function DataTable({ editCell, vendors, accounts }) {
const columns = useMemo(() => table.columns, [table]);
const data = useMemo(() => table.rows, [table]);
const tableInstance = useTable({ columns, data, initialState: { pageIndex: 0 } }, useGlobalFilter, useSortBy, usePagination);
const {
getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
rows,
page,
state: { pageIndex, pageSize, globalFilter },
} = tableInstance;
return (
<Table {...getTableProps()}>
<MDBox component="thead">
{headerGroups.map((headerGroup) => (
<TableRow {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column) => (
<DataTableHeadCell
{...column.getHeaderProps(isSorted && column.getSortByToggleProps())}
width={column.width ? column.width : "auto"}
align={column.align ? column.align : "left"}
sorted={setSortedValue(column)}
>
{column.render("Header")}
</DataTableHeadCell>
))}
</TableRow>
))}
</MDBox>
<TableBody {...getTableBodyProps()}>
{page.map((row, key) => {
prepareRow(row);
return (
<TableRow {...row.getRowProps()}>
{row.cells.map((cell) => {
cell.itemsSelected = itemsSelected;
cell.editCell = editCell;
cell.vendors = vendors;
cell.accounts = accounts;
return (
<DataTableBodyCell
noBorder={noEndBorder && rows.length - 1 === key}
align={cell.column.align ? cell.column.align : "left"}
{...cell.getCellProps()}
>
{cell.render("Cell")}
</DataTableBodyCell>
);
})}
</TableRow>
);
})}
</TableBody>
</Table>
)
}
For example, I want to update the amount in the first object inside the transactions array. What I'm doing now is update the entire companies array, but doing this rerenders the whole table and creates problems. Is there a way I can only update the specific value in a manner that rerenders just the updated field in the table without rerendering the whole table? I've seen other answers but they assume that all values are named object properties.
FYI, I'm not using any state management and would prefer not to use one for now.
You have to copy data (at least shallow copy) to update state:
const nextCompanies = { ...companies };
nextCompanies.transactions[3].amount = 357;
setState(nextCompanies);
Otherwise react won't see changes to the original object. Sure thing you can use memoization to the child component to skip useless rerenders. But I strongly recommend to provide an optimisation only when it is needed to optimise. You will make the code overcomplicated without real profit.
When updating state based on the previous state, you probably want to pass a callback to setCompanies(). For example:
setCompanies((currCompanies) => {
const nextCompanies = [...currCompanies];
// modify nextCompanies
return nextCompanies;
})
Then, in order for React to only re-render the elements that changed in the DOM, you should make sure to set the key prop in each of those elements. This way, React will know which element changed.
// inside your component code
return (
<div>
companies.map(company => (
<Company key={company.id} data={company} />
))
</div>
)
Does this solve the problem? If not, it may be helpful to add some more details so we can understand it fully.
What I'm doing now is update the entire companies array, but doing
this rerenders the whole table and creates problems.
When you say it creates problems what type of problems exactly? How does re-rendering create problems? This is expected behavior. When state or props change, by default a component will re-render.
You seem to be asking two questions. The first, how to update state when only modifying a subset of state (an amount of a transaction). The second, how to prevent unnecessary re-rendering when render relies on state or props that hasn't changed. I've listed some strategies for each below.
1. What is a good strategy to update state when we only need to modify a small subset of it?
Using your example, you need to modify some data specific to a company in a list of companies. We can use map to iterate over each company and and conditionally update the data for the company that needs updating. Since map returns a new array, we can map over state directly without worrying about mutating state.
We need to know a couple things first.
What transaction are we updating?
What is the new amount?
We will assume we also want the company ID to identify the correct company that performed the transaction.
We could pass these as args to our function that will ultimately update the state.
the ID of the company
the ID of the transaction
the new amount
Any companies that don't match the company ID, we just return the previous value.
When we find a match for the company ID, we want to modify one of the transactions, but return a copy of all the other previous values. The spread operator is a convenient way to do this. The ...company below will merge a copy of the previous company object along with our updated transaction.
Transactions is another array, so we can use the same strategy with map() as we did before.
const handleChangeAmount = ({ companyId, transactionId, newAmount }) => {
setCompanies(() => {
return companies.map((company) => {
return company.id === companyId
? {
...company,
transactions: company.transactions.map((currTransaction) => {
return currTransaction.id === transactionId
? {
id: currTransaction.id,
amount: newAmount
}
: currTransaction;
})
}
: company;
});
});
};
2. How can we tell React to skip re-rendering if state or props hasn't changed?
If we are tasked with skipping rendering for parts of the table that use state that didn't change, we need a way of making that comparison within our component(s) for each individual company. A reasonable approach would be to have a reusable child component <Company /> that renders for each company, getting passed props specific to that company only.
Despite our child company only being concerned with its props (rather than all of state), React will still render the component whenever state is updated since React uses referential equality (whether something refers to the same object in memory) whenever it receives new props or state, rather than the values they hold.
If we want to create a stable reference, which helps React's rendering engine understand if the value of the object itself hasn't changed, the React hooks for this are useCallback() and useMemo()
With these hooks we can essentially say:
if we get new values from props, we re-render the component
if the values of props didn't change, skip re-rendering and just use the values from before.
You haven't listed a specific problem in your question, so it's unclear if these hooks are what you need, but below is a short summary and example solution.
From the docs on useCallback()
This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders
From the docs on useMemo()
This optimization helps to avoid expensive calculations on every render.
Demo/Solution
https://codesandbox.io/s/use-memo-skip-child-update-amount-vvonum
import { useState, useMemo } from "react";
const companiesData = [
{
id: 1,
transactions: [
{
id: "10421A",
amount: "850"
},
{
id: "1893B",
amount: "357"
}
]
},
{
id: 2,
transactions: [
{
id: "3532C",
amount: "562"
},
{
id: "2959D",
amount: "347"
}
]
}
];
const Company = ({ company, onChangeAmount }) => {
const memoizedCompany = useMemo(() => {
console.log(
`AFTER MEMOIZED CHECK COMPANY ${company.id} CHILD COMPONENT RENDERED`
);
return (
<div>
<p>Company ID: {company.id}</p>
{company.transactions.map((t, i) => {
return (
<div key={i}>
<span>id: {t.id}</span>
<span>amount: {t.amount}</span>
</div>
);
})}
<button onClick={onChangeAmount}> Change Amount </button>
</div>
);
}, [company]);
return <div>{memoizedCompany}</div>;
};
export default function App() {
const [companies, setCompanies] = useState(companiesData);
console.log("<App /> rendered");
const handleChangeAmount = ({ companyId, transactionId, newAmount }) => {
setCompanies(() => {
return companies.map((company) => {
return company.id === companyId
? {
...company,
transactions: company.transactions.map((currTransaction) => {
return currTransaction.id === transactionId
? {
id: currTransaction.id,
amount: newAmount
}
: currTransaction;
})
}
: company;
});
});
};
return (
<div className="App">
{companies.map((company) => {
return (
<Company
key={company.id}
company={company}
onChangeAmount={() =>
handleChangeAmount({
companyId: company.id,
transactionId: company.transactions[0].id,
newAmount: Math.floor(Math.random() * 1000)
})
}
/>
);
})}
</div>
);
}
Explanation
On mount, the child component renders twice, once for each company.
The button will update the amount on the first transaction just for that company.
When the button is clicked, only one <Company /> component will render while the other one will skip rendering and use the memoized value.
You can inspect the console to see this in action. Extending this scenario, if you had 100 companies, updating the amount for one company would result in 99 skipped re-renders with only one new component rendering for the updated company.
I am mapping through an array from JSON, where I get all the informations I need.
E.g. I have cards of blog posts fetched -> title, short description, published date and its url. So the card is linked based on the url from json - when I click I'll go to new page e.g -> click on blog n.1 -> redirect to mywebsite/blogNumberOne, but of course the page does not exist.
I have another JSON file, which have in the middle name of all the blogs -> so if I take that json and put the right parameter to it -> (blogNumberOne) -> it gives me new json with all the info from that post.
What I need to do is to connect it somehow and make my app to understand that now I click on blog n.1 so it has to go to /blogNumberOne and give me all the correct info from blog n.1. The same applies for each blog post of course.
I know I need to make the request to json, but I just don't know why.
Here is my fetch for printing out the blogs:
useEffect(() => {
fetch('myJSONurl')
.then((resp) => resp.json())
.then((data) => {
setGuides(data.guides);
console.log(data);
});
}, []);
// mapping through
{guides.map((blog) => {
return (
<Card style={{ width: '15rem' }}>
<Card.Img
variant="top"
src={blog.img}
/>
<Card.Body>
<Card.Title>
{blog.title}
</Card.Title>
<a
href={blog.url}
class="stretched-link"
></a>
</Card.Body>
</Card>
Make a page state in you app:
const [page, setpage] = useState(1);
Then on change of page, fetch it's respective data:
useEffect(() => {
// Fetch data according to current page
fetch(`${JSONURL}/${page}`)
},[page]);
// Then map the data
page.map(data => ....)
When you click on a blog post, it should have some id of some sort so you can fetch the data from it:
{
posts.map(post => <BlogPost onClick={() => setPage(/*here mention something unique to each post to fetch data*/post.id)} />)
}
I have an array of Json objects('filteredUsers' in below code) and I'm mapping them to a html table. Json object would look like {id: xyz, name: Dubov}
The below code would display a html table with a single column or basically a list. Each row will have name of user and a grey checkbox(unchecked) next to it initially. I want to select users in the table and when I select or click on any item in table, the checkmark has to turn green(checked).
<table className="table table-sm">
{this.state.filteredUsers && (
<tbody>
{this.state.filteredUsers.map((user) => (
<tr key={user.id}>
<td onClick={() => this.selectUser(user)}>
<span>{user.name}</span> //Name
<div className={user.selected? "checked-icon": "unchecked-icon"}> //Checkmark icon
<span class="checkmark"> </span>
</div>
</td>
</tr>
))}
</tbody>
)}
</table>
I tried setting a 'selected' key to each object. Initially object doesn't have 'selected' key so it will be false(all unchecked). I set onClick method for 'td' row which sets 'selected' key to object and sets it to true. Below function is called onClick of td or table item.
selectUser = (user) => {
user.selected = !user.selected;
};
Now the issue is this will only work if I re-render the page after every onClick of 'td' or table item. And I'm forced to do an empty setState or this.forcedUpdate() in selectUser method to trigger a re-render. I read in multiple answers that a forced re-render is bad.
Any suggestions or help would be highly appreciated. Even a complete change of logic is also fine. My end goal is if I select an item, the grey checkmark has to turn green(checked) and if I click on it again it should turn grey(unchecked). Similarly for all items. Leave the CSS part to me, but help me with the logic. Thanks.
How about something like this:
const Users = () => {
const [users, setUsers] = useState([])
useEffect(() => {
// Fetch users from the API when the component mounts
api.getUsers().then((users) => {
// Add a `selected` field to each user and store them in state
setUsers(users.map((user) => ({ ...user, selected: true })))
})
}, [])
const toggleUserSelected = (id) => {
setUsers((oldUsers) =>
oldUsers.map((user) =>
user.id === id ? { ...user, selected: !user.selected } : user
)
)
}
return (
<ul>
{users.map((user) => (
<li key={user.id} onClick={() => toggleUserSelected(user.id)}>
<span>{user.name}</span>
<div className={user.selected ? "checked-icon" : "unchecked-icon"}>
<span class="checkmark" />
</div>
</li>
))}
</ul>
)
}
I've used hooks for this but the principles are the same.
This looks to be a state issue. When updating data in your react component, you'll need to make sure it's happening in one of two ways:
The data is updated by a component higher up in the tree and then is passed to this component via props.
this will cause your component to re-render with the new props and data, updating the "checked" property in your HTML.
In your case, it looks like you're using this second way:
The data is stored in component state. Then, when you need to update the data, you'd do something like the below.
const targetUser = this.state.filteredUsers.find(user => user.id === targetId)
const updatedUser = { ...targetUser, selected: !targetUser.selected }
this.setState({ filteredUsers: [ ...this.state.filteredUsers, updatedUser ] })
Updating your state in this way will trigger an update to your component. Directly modifying the state object without using setState does not trigger the update.
Please keep in mind that, when updating objects in component state, you'll need to pass a new, full object to setState in order to trigger the update. Something like this will not work: this.setState({ filteredUsers[1].selected: false });
Relevant documentation
I create table that render data which receive from server. But in this table I have cols which not all user should to see. This is my code:
class TableBody extends React.Component {
state = {
columns: {
id: props => (
<td key="id">
{props._id}
</td>
),
title: props => (
<td key="title">
{props.title}
</td>
),
manager: props => (
<td key="manager">
{props.manager}
</td>
)
},
hiddenColumns: {
user: ['manager']
}
}
In state I init my columns and add columns restrictions for user (he can not see manager column). In render I do next:
render() {
const hiddenColumns = this.state.hiddenColumns[this.props.role] || [];
const columns = Object.keys(this.state.columns).filter(key => {
return hiddenColumns.indexOf(key) === -1
});
return this.props.companies.map(company => (
<tr key={offer._id}>
{columns.map(element => this.state.columns[element](company))}
</tr>
));
}
I get hidden columns for current user and filter key in columns. After this I use map to go over data which receive from server and inside map I go over for each filtered columns and send element (props).
In the future, more columns will be added to this table and make this:
{columns.map(element => this.state.columns[element](company))}
will not be effective.
Maybe I can create main template and after init remove columns which user should not to see, but I don't know how.
Please help me
Thank you
I think you are doing this completely wrong. you should never filter data specific to user role on client side.
Ideally such data should be filtered on server side and then send to client only role specific data.
With your current approach user can simply inspect browser network tab and read all other restricted columns.
I am facing an issue to order the rows of a fetched API data dynamically, i.e if the rows data within the API is changed (+/- row) to render it automatically. I am trying to print the data fetched from an API into a table. Anyhow the columns are well printed dynamically, but the function that I am using for columns this.state.columns.map(( column, index ) => doesn't function in the same way for the rows. I think i am misleading the ES6 standard, but I am not sure. Here is how it's look like, if the rows are not hardcoded.
Here is my code sample:
class App extends React.Component
{
constructor()
{
super();
this.state = {
rows: [],
columns: []
}
}
componentDidMount()
{
fetch( "http://ickata.net/sag/api/staff/bonuses/" )
.then( function ( response )
{
return response.json();
} )
.then( data =>
{
this.setState( { rows: data.rows, columns: data.columns } );
} );
}
render()
{
return (
<div id="container" className="container">
<h1>Final Table with React JS</h1>
<table className="table">
<thead>
<tr> {
this.state.columns.map(( column, index ) =>
{
return ( <th>{column}</th> )
}
)
}
</tr>
</thead>
<tbody> {
this.state.rows.map(( row ) => (
<tr>
<td>{row[0]}</td>
<td>{row[1]}</td>
<td>{row[2]}</td>
<td>{row[3]}</td>
<td>{row[4]}</td>
<td>{row[5]}</td>
<td>{row[6]}</td>
<td>{row[7]}</td>
<td>{row[8]}</td>
<td>{row[9]}</td>
<td>{row[10]}</td>
<td>{row[11]}</td>
<td>{row[12]}</td>
<td>{row[13]}</td>
<td>{row[14]}</td>
<td>{row[15]}</td>
</tr>
) )
}
</tbody>
</table>
</div>
)
}
}
ReactDOM.render( <div id="container"><App /></div>, document.querySelector( 'body' ) );
Instead, I was able to print the rows harcoded, if I give value to the 'td' elements, but I want to print it dynamically, in case the data within the API has been changed.
You are welcome to contribute directly to my Repo: Fetching API data into a table
Here is how looks like my example, when the rows values has been hardcoded within 'td' elements.
Any suggestions will be appreciated!
Just as you are iterating over columns and rows you could add an additional loop for iterating over row cells.
For example:
this.state.rows.map(row => (
<tr>{row.map(cell => (
<td>{cell}</td>
))}
</tr>
))
Note that you probably want to add a key prop:
Keys help React identify which items have changed, are added, or are
removed. Keys should be given to the elements inside the array to give
the elements a stable identity