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`
// ]
// })
// }
Related
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 new to React and I am trying to use state for the first time. For some reason statelist.name does not return anything. Do I need to use a constructor ? Any Help would be great.
import React from 'react';
class HorizantScroller extends React.Component {
state = {
statelist: [
{name: "Brands",
items: ["1", "2", "3"]
},
{name: "Films",
items: ["f1", "f2", "f3"]
},
{name: "Holiday Destination",
items: ["f1", "f2", "f3"]
}
]
};
render() {
const { selected } = this.state;
// Create menu from items
const menu = Menu(list, selected);
const {statelist} = this.state;
return (
<div>
<div name={statelist.name}></div>
</div>
);
}
}
export default HorizantScroller;
StateList is an array, you will need to specify the index of the state first and the get the property 'name'. Example: statelist[0].name
You could do something like this to make it dynamic:
{statelist.map((list,index)=>{
return (
<div key={index} name={list.name}></div>
);
})
}
This way you would have all the values in the statelist. replace the <div name={statelist.name}></div> with the above code.
Notice how i have added a key to the div. this is important for react to distinguish between the divs and also it'll complain in the console if you don't do it :).
Your problem is not with the react state, is with the data types you're using. First of all statelist is not an object so you can't use statelist.name, it's an array so you need to pass an index of the position of the array you're trying to retrieve, it should be something like statelist[0].name.
If you want to access all the names of statelist not just the first item you need iterate in the array and do something like this:
{statelist.map((element,index) => (
<div key={index} name={element.name}></div>
)) }
statelist is an array and does not have a field named name. The elements of the array has the field name. This means you have to access an element. You can do this with an index:
let index = 0;
<div name={statelist[0].name}></div>
If you want to get the names of all elements you can use the map function:
{statelist.map(element => {
return (
<div name={element.name}></div>
);
})}
This question may be more about opinion than fact, but I'm unsure so thought I'd ask.
I'm building some forms which will display data and allow edits, the field data comes from props (as a parent component is using a GraphQL query to pull a larger amount and pass to each child).
I'm finding some input data is evaluating to null (as it's not passed back from the query) which throws a warning as inputs don't like being assigned null values.
My question is, when passing these values, what's the cleanest way to run checks on each variable and assign an empty string if needed?
So far the two options i've tried are:
Conditionally assign each to the state object, but this feels clunky and is a lot of code:
const [state, setState] = useState({
telephone: props.telephone ? props.telephone : '',
nickname: props.nickname ? props.nickname : ''
etc...
});
Or to define a function which maps over props and checks values, before setting state:
useEffect( () => {
let state_arr = {};
Object.keys(props).map( (key) => {
if( !props[key] ) state_arr[key] = '';
else state_arr[key] = props[key];
} );
setState(state_arr);
}, [] )
Honestly this feels cleaner than the first option, but there are a number of places this will occur and to have to do this in each feels counter productive.
Any help/insight appreciated.
EDIT: It turns out OP is using Material UI for this..Meaning, the reason the input is showing a warning is due to Material UI using PropTypes. I suggested that OP create a wrapper for the <Input /> component and pass through all props. Inside of the wrapper component you can just do: <InputWrapper value={props.value || ""} {...rest} /> and this covers things..
Live Demo
InputWrapper:
import React from 'react';
import { Input } from '#material-ui/core';
export default function InputWrapper({ value, ...rest }) {
return <Input value={value || ""} {...rest} />
}
InputWrapper In Use:
import React, { useState, useEffect } from 'react';
import { render } from 'react-dom';
import InputWrapper from './InputWrapper.js';
function App(props) {
const [state, setState] = useState({});
useEffect(() => {
setState({
name: props.name,
age: props.age,
hairColor: props.hairColor,
})
}, [props.name, props.age, props.hairColor]);
const handleChange = (event, inputType) => {
setState({...state, [inputType]: event.target.value})
}
return(
<div>
{/* Shows that you can pass through native <Input /> props: */}
{/* state.name is null here! Warning is NOT thrown in the console! */}
<InputWrapper value={state.name} fullWidth onChange={e => setState({...state, name: e.target.value})} />
<InputWrapper value={state.name} multiline onChange={e => setState({...state, name: e.target.value})} />
{Object.keys(state).map((item, index) => {
return (
<div>
<InputWrapper
key={`${item}_${index}`}
value={state[item]}
onChange={e => handleChange(e, item)} />
</div>
);
})}
</div>
);
}
render(
<App name={null} age={44} hairColor="blue" />,
document.getElementById('root')
);
ORIGINAL ANSWER:
What is your use case? There is no reason to run checks and assign empty strings...
If you are trying to enforce that certain properties are used, please look into PropTypes... If you are not wanting to enforce that certain props get used, I would recommend checking for a value during use of the variable. Even if you set it to an empty string initially, you could still encounter errors down the line - I don't understand what you gain from an empty string.
I don't understand the use case - can you elaborate more on why you need to set it to an empty string?
If you really wanted to, you could verify like: useState({two: props.two || ""}) ...but it is still unnecessary..
// Notice how prop "two" is not being used..
function Test(props) {
const [state, setState] = React.useState({
one: props.one,
two: props.two
})
return(
<div>
<p>{state.one}</p>
<p>Even though <code>state.two</code> does not exist, there are no errors.. (at least for this demonstration)</p>
<input type="text" value={state.two} />
<input type="text" value={state.two} defaultValue={"default"} />
<p><i>If you really wanted to, you could verify like:</i><code>useState({two: props.two || ""})</code><i>...but it is still unnecessary..</i></p>
</div>
);
}
ReactDOM.render(<Test one="I AM ONE" />, document.body)
code {
margin: 0 10px;
padding: 3px;
color: red;
background-color: lightgray;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
What about making method KickOutNullValues() which will do what you want and then you can reuse it everywhere you need. That would be more elegant.
This is a tough question, i don't know the right answer. You already tried two ways, the different way that I normally do is,
If you just want to get the display right, i would just do
<Telephone data={props.telephone} />,
const Telephone = ({data}) => { if (!data) return null }
I found this is to allow the child component to ensure the validity of this issue rather than sorting out the data in the parent API level.
Telephone.defaultProps = {
data: ''
}
This further ensures that if the data is null, it'll be reset to '' by the defaultProps
The reason I prefer this way most of time is that I don't really want to mess with the origin TRUTH of the API data.
Of course your ways might be better if you do want to ensure the data is valid at all time :)
Your code will start to have spaghetti-like qualities if you put the raw algorithm inside your callback. I recommend writing a function outside.
Your usage of Array#map is not correct, or rather you are using it in an unintended way. Array#map is used to construct an entirely new array. You are simulating Array#forEach. Also, you're performing a falsey conditional check. null is one of many values that are considered false in JavaScript. Namely, your pain points will probably be undefined, 0, and ''. If the only invalid return value is null, then check for null explicitly.
The enumerable that is for your intended use case is Array#reduce:
function nullValueReplacer(obj) {
return Object.entries(obj).reduce((newStateArr, [currentKey, currentValue]) => {
if (currentValue === null) {
newStateArr[currentKey] = ''
} else {
newStateArr[currentKey] = currentValue
}
return newStateArr
}, {});
}
As a side note, you might want to update your variable names. It's pretty deceptive that you have a variable called state_arr that is an object.
Array of objects - little fix
You should not use key with map..
think about this: (similar to yours)
useEffect(() => {
let state_arr = Object.keys(props).map(prop => prop ? {prop} : { prop: '' });
setState(state_arr);
}, [])
By using this code you make an array with object and have easy access for every item
In case there is no nickname it will look like that:
[{ telephone: '245-4225-288' }, { nickname: '' }]
What do you think?
Let's say i have a tabbed component. Each tab presents a person object. The user can switch the active tab, modify or remove each one and even change their order:
state={
activeTab:0,
tabs:[
{
name:'John',
age:34
},
{
name:'Bob',
age:31
},
]
}
Let's say i want to modify one of the fields, of a specific tab(person). I could have this function:
modifyTab = (index, prop, value) => {
const tabs = [...this.state.tabs];
const tab = tabs[index];
tab[prop] = value;
this.setState({ tabs })
}
The problem with this, is that it relies on the index of the given tab. But what if the tab index changes, if i provide, let's say, the ability of switching the tab order?(like the browsers do for their tabs).
Or what if i need to register some callback for an a-sync operation, that might be called when the relevant person sits in a different tab(maybe the tab was moved from 1 to 0, by the time the callback was called)?
Is there any way to just rely on object reference, regardless of id, index or any other "identifier", which makes the code much more complicated than it needs to be?
For those who are familiar with VueJS and Angular, i'm sure you know how easy it is to modify objects, being that you do not need to return a new state tree on each change.
If you are changing the order of the array, you cannot rely on the array index for the key prop when you are rendering. One common way around this is to add a unique property to every object in the array and use that instead, e.g. an id.
Passing in the entire object reference to modifyTab would be perfectly fine. You could figure out what object in the array that is with a simple indexOf.
Example
class App extends React.Component {
state = {
tabs: [
{
name: "John",
age: 34,
id: 1
},
{
name: "Bob",
age: 31,
id: 2
}
]
};
modifyTab = (tab, prop, value) => {
this.setState(prevState => {
const tabs = [...prevState.tabs];
const index = tabs.indexOf(tab);
tabs[index] = { ...tabs[index], [prop]: value };
return { tabs };
});
};
render() {
return (
<div>
{this.state.tabs.map(tab => (
<span
key={tab.id}
onClick={() => this.modifyTab(tab, "name", Math.random())}
style={{ margin: '0 10px' }}
>
{tab.name}
</span>
))}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
New to React.
I have a handler, as follows, that updates state of an array. The data is a set of animal pairs.
class Animal extends Component {
state = {
pairs: [
{ fromAnimal: 'Dog', toAnimal: 'Cat' },
{ fromAnimal: 'Lion', toAnimal: 'Tiger' },
{ fromAnimal: 'Rabbit', toAnimal: 'Bear' }
]
};
closePairHandler = (fromAnimal, toAnimal) => {
let newPairs = this.state.pairs.filter((pair) => {
return !(pair.fromAnimal === fromAnimal && pair.toAnimal === toAnimal);
});
console.log('pairs', newPairs); // This shows that the correct pair was removed from the array.
this.setState({ pairs: newPairs });
};
render() {
return (
<div>
{
this.state.pairs.map((pair, index) => {
return <SomeComponent key={index} pair={pair} closePair={(fromAnimal, toAnimal) => this.closePairHandler(fromAnimal, toAnimal)} />;
}
}
</div>
);
};
};
export default Animal;
This is a super simplified version of the code I have. BUT, when the closePairHandler is called to remove an animal pair (for example, Lion/Tiger). The console.log in the closePairHandler shows that the array has been updated successfully.
However, when the components render. It is removing the LAST component in the array and not the one that was selected. It's reducing the array size by 1, but not removing the correct item in the mapping (in render), althought the closePairHandler console.log is showing the array correctly updated before setting the state.
Can anyone explain to me what is going on here?
Thanks again!
You are not providing the key for your mapped data while rendering SomeComponent and hence react is not able to correctly identify what element got changed. You can use index as the key if you don't have a unique id in your pair object else you should use that for performance reasons
return (
<div>
{
this.state.pairs.map((pair, index) => {
return <SomeComponent key={index} pair={pair} closePair={(fromAnimal, toAnimal) => this.closePairHandler(fromAnimal, toAnimal)} />;
}
}
</div>
);