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.
Related
I have a diary object with 2 meals
function Magicdiary() {
const [diary, setDiary] = React.useState<MagicDiaryDay[]>([
{ mealName: "Breakfast", ingredient: null },
{ mealName: "Lunch", ingredient: null },
]);
return (
<div>
<p>meal 1: {diary[0].ingredient?.productName}</p>
<Button onClick={() => console.log(diary[0].ingredient?.productName)}>
log diary
</Button>
{diary.map((meal, index) => {
return (
<MealComponentForMagicDiary
diary={diary}
setDiary={setDiary}
index={index}
/>
);
})}
</div>
);
}
I have selection of ingredients that I call from my backend, and everytime I select current ingredient, I set it to the diary:
// MealComponentForMagicDiary
useEffect(() => {
if (hit) {
const diaryCopy = diary;
diaryCopy[index].ingredient = {
productName: hit.productName,
nutrients: {
"energy-kcal_serving": hit.calories,
protein_serving: hit.protein,
carbohydrates_serving: hit.carbs,
fat_serving: hit.fats,
},
};
setDiary(diaryCopy);
}
}, [hit, selectedHit]);
As you can see meal 1 is empty, but when I log it on the console I can see the correct productName what is the cause of this bug?
You are updating the state in the wrong way, you are mutating the original array that is overwriting the exiting array, Instead, you need to do it in an immutable way that is providing a new Instance of diary whenever you want to update it, you can do in the following way
useEffect(() => {
if (hit) {
const diaryCopy = diary.map((d, ind) => {
if (ind === index) {
// The Diary ingredient you want to update
d.ingredient = {
productName: hit.productName,
nutrients: {
"energy-kcal_serving": hit.calories,
protein_serving: hit.protein,
carbohydrates_serving: hit.carbs,
fat_serving: hit.fats,
}
};
}
return d;
} );
setDiary(diaryCopy);
}
}, [hit, selectedHit]);
From my limited react experience, when funny things like this happen I have a few go-to methods to try. One of them is to call an empty function when passing a function down as a prop. I.e. instead of:
<MealComponentForMagicDiary
diary={diary}
setDiary={setDiary}
index={index}
/>
try:
<MealComponentForMagicDiary
diary={diary}
setDiary={() => setDiary}
index={index}
/>
I'd love to know why this works sometimes, if anybody (does anybody?) understands react properly.
My fingers are crossed that it works for you!
It's hard to say exactly with this given information but I'm inclined to say this:
You're not getting the information you want because when the component renders, it's not there. Take for example this:
<p>meal 1: {diary[0].ingredient?.productName}</p>
You check if ingredient exists but are you sure diary[0] exists? Since you're setting this data elsewhere in a useEffect, I suspect that it's not available at render - even though you can console it.
I suggest using the React Developer Tools to look at your component tree and see what that state looks like when it's rendered.
That's all I can guess without more code.
Suppose I have long list (let's assume there is no pagination yet) where each list item has input and ability to update own value (as a part of collection). Let's say code looks something like that:
const initItems = [
{ id: 0, label: "Hello world" },
...
{ id: 100, label: "Goodby" }
];
function List() {
const [items, setItems] = React.useState([...initItems]);
const handleChange = React.useCallback((e, id) => {
setItems(items.map(item => {
if (item.id === id) {
return {
...item,
label: e.target.value
}
}
return item;
}));
}, [items]);
return (
<ul>
{items.map(({ id, label }) => {
return (
<Item
id={id}
key={id}
label={label}
onChange={handleChange}
/>
)
})}
</ul>
)
}
// Where Item component is:
const Item = React.memo(({ onChange, label, id }) => {
console.log('Item render');
return (
<li>
<input type="text" value={label} onChange={e => onChange(e, id)} />
</li>
)
});
Looks pretty straightforward, right? While wrapping Item component with React.memo() what I wanted to achieve is to avoid re-render of each Item when some of the Item's gets updated. Well, I'm not sure it should works with this strategy, since each Item is a part of collection (items) and when I update any Item then items gets mapped and updated. What I did try - is to write custom areEqual method for Item component, where I do comparison of label value from props:
function areEqual(prev, next) {
return prev.label === next.label;
}
however with this approach the behaviour of updating items breaks down completely and updating next item reset previous updates and so on (I even could not observe any pattern to explain).
So the question: is it possible to avoid re-rendering of every item in such collection while having ability to update value of individual item?
Your problem here that you change callback on each render. So, you change callback, it changes onChange and this, in turn, runs rerender. To avoid it you can use updater function with setState.
const handleChange = React.useCallback((e, id) => {
// I made separate function so it would be easier to read
// You can just write `(items) =>` before your `items.map` and it will work
function updater(items) {
// we have freshest items here
return items.map((item) => {
if (item.id === id) {
return {
...item,
label: e.target.value,
};
}
return item;
});
}
// pass function
setItems(upadter);
// removed items from dependencies
}, []);
This way, your updater function will always get current value of state into parameters, and your props will update for actually updated item. Another solution would be to write custom updater that compares all values, but onChange. This is ok in short term, but this can become complex and cumbersome to maintain.
Here is live example: https://codesandbox.io/s/unruffled-johnson-ubz1l
Well, I am trying to figure out how to play with an array within useEffect hook. Basically, I want to know how to pass index as a parameter so I don't have to grab the indexes manually.
I am fetching some data from the backend in order to edit some form fields.
The field I am having trouble, is a field that you could create dynamically. There could be only 1 or 100.
This is the component:
// HERE IS WHERE I AM ATTEMPTING TO DO WHAT I NEED
useEffect(() => {
// So there is only one name key.
// I need that to be dynamic. To accept as many name keys as possible.
startupFourthStepFormActionHandler({
products_or_services:
[
{
name:
startupProductsOrServicesInfo.productsorservices &&
startupProductsOrServicesInfo.productsorservices[0].name,
},
] || [],
});
}, []);
return (
<div>
{productsOrServicesInputs.map((input, index) => (
<div key={input}>
// THESE ARE DYNAMIC INPUTS. SO THERE COULD 1 OR 100
<FormField
value={startupFourthStepForm.products_or_services[index].name}
/>
// LET'S SAY THIS WAS CREATED DYNAMICALLY
<FormField
value={startupFourthStepForm.products_or_services[index].name}
/>
</div>
))}
</div>
);
};
If you see the component above, on useEffect I am doing this:
return startupFourthStepFormActionHandler({
products_or_services:
[
{
name:
startupProductsOrServicesInfo.productsorservices &&
startupProductsOrServicesInfo.productsorservices[0].name,
},
] || [],
});
I would like to turn that into something like:
return startupFourthStepFormActionHandler({
products_or_services:
[
{
name:
startupProductsOrServicesInfo.productsorservices &&
startupProductsOrServicesInfo.productsorservices[INDEX].name,
},
] || [],
});
Or something which allows me to have as many name keys as needed. Like:
return startupFourthStepFormActionHandler({
products_or_services:
[Here I should grab an array of 100 hundred objects with key called name] || [],
});
So I can do something like:
startupProductsOrServicesInfo.productsorservices[index].name
As you see I have this startupProductsOrServicesInfo.productsorservices[0].name but I need that be the proepr index of the item in the array. For now it is only grabbing the index 0, I need that index to be grabbed dynamically.
In the useEffect method on the component, you may see this
startupProductsOrServicesInfo.productsorservices which is an API call and returns this ->
{
"success": true,
"productsorservices": [
{
"name": "Software",
},
{
"name": "Hardware",
}
]
}
So all I am trying to is to set the value coming from the backend here on this value ->
value={startupFourthStepForm.products_or_services[index].name} which you may in the component <FormField />.
I need to do that in the useEffect hook.
What am I missing?
TL;DR
For no the component is doing what I need, but it only grabs the first index of the array, startupProductsOrServicesInfo.productsorservices[0].name and I need it to be grabbed dynamically.
Fundamentally useEffect hooks are just a way to control the timing of certain code. Otherwise they're not special at all, and so to solve your problem you just need regular old React solutions.
If you want to pass the index into your component, you simply do so with props:
const YourComponent = props => {
// HERE IS WHERE I AM ATTEMPTING TO DO WHAT I NEED
useEffect(() => {
// ..
startupProductsOrServicesInfo.productsorservices[props.index].name,
// ..
}
And then in pass the index in in your parent component:
return <YourComponent index={1} />
If you want your component to remember the index, you have to use state instead:
const YourComponent = () => {
const [index, setIndex] = useState(0);
// HERE IS WHERE I AM ATTEMPTING TO DO WHAT I NEED
useEffect(() => {
// ..
startupProductsOrServicesInfo.productsorservices[index].name,
If you want an event to change that index, for instance if you wanted a button in your component to increment it, you would use the setIndex function returned by useState:
const YourComponent = () => {
const [index, setIndex] = useState(0);
// HERE IS WHERE I AM ATTEMPTING TO DO WHAT I NEED
useEffect(() => {
// ..
startupProductsOrServicesInfo.productsorservices[index].name,
});
const incrementIndex = () => setIndex(index + 1);
return <button onClick={incrementIndex}>Click Me!</button>;
I have a form with several layers of child components. The state of the form is maintained at the highest level and I pass down functions as props to update the top level. The only problem with this is when the form gets very large (you can dynamically add questions) every single component reloads when one of them updates. Here's a simplified version of my code (or the codesandbox: https://codesandbox.io/s/636xwz3rr):
const App = () => {
return <Form />;
}
const initialForm = {
id: 1,
sections: [
{
ordinal: 1,
name: "Section Number One",
questions: [
{ ordinal: 1, text: "Who?", response: "" },
{ ordinal: 2, text: "What?", response: "" },
{ ordinal: 3, text: "Where?", response: "" }
]
},
{
ordinal: 2,
name: "Numero dos",
questions: [
{ ordinal: 1, text: "Who?", response: "" },
{ ordinal: 2, text: "What?", response: "" },
{ ordinal: 3, text: "Where?", response: "" }
]
}
]
};
const Form = () => {
const [form, setForm] = useState(initialForm);
const updateSection = (idx, value) => {
const { sections } = form;
sections[idx] = value;
setForm({ ...form, sections });
};
return (
<>
{form.sections.map((section, idx) => (
<Section
key={section.ordinal}
section={section}
updateSection={value => updateSection(idx, value)}
/>
))}
</>
);
};
const Section = props => {
const { section, updateSection } = props;
const updateQuestion = (idx, value) => {
const { questions } = section;
questions[idx] = value;
updateSection({ ...section, questions });
};
console.log(`Rendered section "${section.name}"`);
return (
<>
<div style={{ fontSize: 18, fontWeight: "bold", margin: "24px 0" }}>
Section name:
<input
type="text"
value={section.name}
onChange={e => updateSection({ ...section, name: e.target.value })}
/>
</div>
<div style={{ marginLeft: 36 }}>
{section.questions.map((question, idx) => (
<Question
key={question.ordinal}
question={question}
updateQuestion={v => updateQuestion(idx, v)}
/>
))}
</div>
</>
);
};
const Question = props => {
const { question, updateQuestion } = props;
console.log(`Rendered question #${question.ordinal}`);
return (
<>
<div>{question.text}</div>
<input
type="text"
value={question.response}
onChange={e =>
updateQuestion({ ...question, response: e.target.value })
}
/>
</>
);
};
I've tried using useMemo and useCallback, but I can't figure out how to make it work. The problem is passing down the function to update its parent. I can't figure out how to do that without updating it every time the form updates.
I can't find a solution online anywhere. Maybe I'm searching for the wrong thing. Thank you for any help you can offer!
Solution
Using Andrii-Golubenko's answer and this article React Optimizations with React.memo, useCallback, and useReducer I was able to come up with this solution:
https://codesandbox.io/s/myrjqrjm18
Notice how the console log only shows re-rendering of components that have changed.
Use React feature React.memo for functional components to prevent re-render if props not changed, similarly to PureComponent for class components.
When you pass callback like that:
<Section
...
updateSection={value => updateSection(idx, value)}
/>
your component Section will rerender each time when parent component rerender, even if other props are not changed and you use React.memo. Because your callback will re-create each time when parent component renders. You should wrap your callback in useCallback hook.
Using useState is not a good decision if you need to store complex object like initialForm. It is better to use useReducer;
Here you could see working solution: https://codesandbox.io/s/o10p05m2vz
I would suggest using life cycle methods to prevent rerendering, in react hooks example you can use, useEffect. Also centralizing your state in context and using the useContext hook would probably help as well.
laboring through this issue with a complex form, the hack I implemented was to use onBlur={updateFormState} on the component's input elements to trigger lifting form data from the component to the parent form via a function passed as a prop to the component.
To update the component's input elelment, I used onChange={handleInput} using a state within the compononent, which component state was then passed ot the lifting function when the input (or the component as a whole, if there's multiple input field in the component) lost focus.
This is a bit hacky, and probably wrong for some reason, but it works on my machine. ;-)
I was watching an React Tutorial and the instructor did not explain a part very well for me to understand. He was basically trying teach how to render the list dynamically in three different input boxes. Whatever is typed in each input box will render to the according div element above it. And the instructor told us we should not touch the state directly which was where this code got more complicated. Any easier way to write this code? Not understanding him. The code that instructor instructed is in the nameChangeHandler function. Please see code below. Thanks!
import React, { Component } from 'react';
import './App.css';
import Person from "./Person/Person"
class App extends React.Component {
state={
persons: [
{id: 1,name: "Max", age:28 },
{id:2,name: "Manu", age: 29},
{id:3, name: "Stephanie", age: 26 }
],
showPersons: false
}
deletePersonHandler=(index)=> {
const persons = [...this.state.persons];
persons.splice(index, 1)
this.setState({ persons: persons});
console.log(persons)
}
nameChangedHandler = (e, id ) => {
const personIndex = this.state.persons.findIndex(p=> {
return p.id === id;
})
const person = {
...this.state.persons[personIndex]
};
person.name= e.target.value;
const persons = [...this.state.persons];
persons[personIndex] = person;
this.setState({
persons: persons
})
}
togglePersonsHandler=()=> {
const showing = this.state.showPersons;
this.setState({ showPersons: !showing })
}
render() {
const style={
backgroundColor: "white",
font:"inherit",
border:"1px solid blue",
padding:"8px",
cursor:"pointer"
}
let persons=null;
if(this.state.showPersons) {
persons=(
<div>
{this.state.persons.map((person, index)=> {
return(
<Person
key={person.id}
changed={(e)=>this.nameChangedHandler(e, person.id)}
click={()=>this.deletePersonHandler(index)}
name={person.name}
age={person.age}/>
)
})}
</div>)
}
return (
<div className="App">
<h1>Hi, Im a React App</h1>
<p>This is really working!!!</p>
<button style={style} onClick={this.togglePersonsHandler}>Toggle Persons</button>
{persons}
</div>
);
}
}
export default App;
As per your request in the comments here is a brief explenation of this code:
nameChangedHandler = (e, id ) => {
const personIndex = this.state.persons.findIndex(p=> {
return p.id === id;
})
What you see is an arrow function. For the purpose of this entire answer, treat them as normal function (it is not the same, however it could be done with regular functions as well). Semantically speaking, arrow functions or regular functions does not change what the code is doing/its intention so I will not go into details, you should just be aware of what you are seeing. If you are unfamiliar with them though, you should read up on it, they are very useful. The signature for an arrow function is either (a,b) => {}, a => {} or a => <expression>. So roughly speaking the above can be logically interpreted as function(e,id){} and function(p){} just to clear that up before I proceed (it would not work if written that way, but that is the message it conveys).
The code itself extracts the index of the person that matches the id parameter that you passed to the nameChangeHandler. This is done using findIndex, a function that iterates through the array (.persons array of your state in this case), and returns the index of the first element that passes the test function given. This index is then stored inside a variable for usage later in the code.
The values
e and id are coming from invocation of the function itself, I cannot give you more detail, since I do not see what the <Person> class is, but it is safe to assume that this handler is being attached to an input field. Once a change happens via the onChange handler on an input field, react will trigger a handler and pass an event containing the event data to it. Your handler is actually not the nameChangeHandler function, it is an arrow function which takes an event e, and then calls the nameChangeHandler passing both the event e as well as the id for the person, you can see that here changed={(e)=>this.nameChangedHandler(e, person.id)}. Rest of the values are read from your state.
Let's continue with the code:
const person = {
...this.state.persons[personIndex]
};
What we have here is called a spread. It essentially "unpacks and repacks" the object or an array, you can read more about it on the MDN link given. This is a powerful new feature of ES6 that makes life a lot easier.
So the above code is used to cleverly shallow copy a person object from the array into a new local variable (or rather a const, since variable would imply a possibility of change).We do this because in javascript, object data is stored by reference, so we cannot simply change the person object inside the initial array, that would mutate the state. We do not want to mutate the state. Immutable is the key here.
person.name= e.target.value;
Following that we have a simple asignment. The new person object we just created is an exact (sort of) copy of what the person inside the state's .persons array was, and that is no good, we want to change the name, so we do exactly that. We access the event e and read the value of the target that triggered it, assign the new value to our person object, and now we have a "changed man" (pun intended).
What is left for us to do is, push these changes into the state so that a new render can show them, so we do:
const persons = [...this.state.persons];
persons[personIndex] = person;
This code again uses the spread to clone/copy an old array from the state into a new local array persons. It is equvivalent to using const persons = this.state.persons.slice(). You can read more about .slice() on the MDN (intentionally not leaving a direct link for you so that you search for it and learn that part as well, MDN is really a great source for documentation of all sorts and getting to know your way around that website is a lifesaver.). Lastly, after the array is cloned, we find the original person and replace it with out local person object, that has a changed name.
this.setState({
persons: persons
})
Lastly we use the .setState method that react provides (see documentation) to immutably change the state. This change will trigger a new render() and you will be able to see the changes in the UI. The .setState() itself operates by doing a shallow merge. This means that only the properties that you pass to the method itself will be changed/added to the state, rest of the properties will be kept, as-is. Since the only thing we pass is a different array of persons which is our local array, with the changed person, that is the only thing that changes.
When updating local state based off of current local state, you should be using the setState callback pattern, because of the way calls to this.setState are batched by React.
Also, shallow-copy and update is perfectly fine, however you could also use the standard Array.prototype.map method to perform the update:
nameChangedHandler = (e, id ) => {
this.setState((prevState) => {
const persons = prevState.persons.map(person, i => {
return i === id ? { ...person, name: e.target.name } : person;
});
return {
persons: persons,
};
});
}
import React, { Component} from 'react';
import './App.css';
import Person from './Person/Person';
class App extends Component{
state = {
persons : [
{id:1, name : 'sohan', age : 28},
{id:2, name : 'Rohan', age : 29},
{id:3, name : 'Stephani', age : 21}
],
otherState : 'hiii',
showPersons : false
}
nameChangeHandler = (event, id)=>{
const personInedex = this.state.persons.find(p => {
return p.id === id;
})
personInedex.name = event.target.value;
this.setState({persons:this.state.persons})
}
toglePerson = ()=>{
this.setState({showPersons:!this.state.showPersons})
}
delete = (item) =>{
const persons = [...this.state.persons];
persons.splice(item, 1);
this.setState({persons: persons});
}
render(){
const style = {
backgroundColor: '#88dc707d',
margin: 0,
font: 'inherit',
padding : '15px',
cursor: 'pointer',
color: 'white',
boxShadow: '15px 10px 12px grey',
borderRadius: '22px 5px 22px 5px'
}
let persons = null
if(this.state.showPersons){
persons = (
<div>
{this.state.persons.map((item, index) => {
return <Person key={item.id}
name={item.name}
age={item.age}
onchange={(event) => this.nameChangeHandler(event, item.id)}
deleteName={()=> this.delete(index)} />
})}
</div>
)
}
return (
<div className="App">
<h1> hii i am React App</h1>
<button
style={style}
onClick={this.toglePerson}>Togle Name</button>
{persons}
</div>
);
}
}
export default App;
// state = {
// persons : [
// {name : 'sohan', age : 28},
// {name : 'Rohan', age : 29},
// {name : 'Stephani', age : 21}
// ],
// otherState : 'hiii'
// }
// switchNameHandler = () => {
// console.log(this.state)
// this.setState({
// persons : [
// {name : 'Sudan Lal', age : 28},
// {name : 'Rohan', age : 29},
// {name : 'Stephani', age : 25}`enter code here`
// ]
// })
// }