I have an array of 16 objects which I declare as a state in the constructor:
this.state = {
todos:[...Array(16)].map((_, idx) => {
return {active: false, idx}
}),
}
Their status will get updated through an ajax call in ComponentDidMount.
componentDidMount()
{
var newTodos = this.state.todos;
axios.get('my/url.html')
.then(function(res)
{
newTodos.map((t)=>{
if (something something)
{
t.active = true;
}
else
{
t.active = false;
}
}
this.setState({
todos:newTodos,
})
}
}
and then finally, I render it:
render(){
let todos = this.state.todos.map(t =>{
if(t.active === true){
console.log('true'}
else{
console.log('false')
}
})
return (
<div></div>
)
}
They all appear as active = false in the console, they never go into the if condition. When
I print out the entire state it appears not to be updated in the render method. In the console it says "value below was just updated now".
I thought changes to the state in ComponentWillMount will call the render function again?
How do I make that React will accept the new values of the state?
componentDidMount()
{
var newTodos = []; // <<<<<<<<<<<<<
axios.get('my/url.html')
.then(function(res)
{
newTodos = this.state.todos.map((t)=>{ //<<<<<<<<<<<<<<<
if (something something)
{
t.active = true;
}
else
{
t.active = false;
}
return t; //<<<<<<<<<<<<<<<<<<
} // <<<<< are you missing a semi-colon?
this.setState({
todos:newTodos,
})
}
}
The map() argument (in your code) is a function, not an expression, so an explicit return must be provided. I.E.:
xxx.map( t => ( "return t is implicit" ) );
xxx.map( t => { "return t must be explicit" } );
And, as #DanielKhoroshko points out, your new variable points to this.state. And of course never, never, ever alter this.state directly. Since map() returns a new array, not the original as altered, that's why we use map() and not forEach()
That is because you are actually not providing any new state, but mutating it instead.
React uses shallow comparison be default (where to objects are equal if they reference the same memory address). And that's exactly what's happening here:
var newTodos = this.state.todos; // newTodos === this.state.todos
this.setState({ todos:newTodos }) // replaces two equal addresses, so it simply doesn't change anything
The easiest solution, though probably not the most performant would be to clone your todos array:
var newTodos = [...this.state.todos]; // newTodos !== this.state.todos
Related
I have two states defined like so:
const [productProperties, setProductProperties] = useState<
PropertyGroup[] | null
>(null);
const [originalProductProperties, setOriginalProductProperties] = useState<
PropertyGroup[] | null
>(null);
The first one is supposed to be updated through user input and the second one is used later for a comparison so that only the PropertyGroup's that have changed values will be submitted via API to be updated.
I have done this a thousand times before, but for some reason when I change the name value for a PropertyGroup and update the state for 'productProperties' like so:
(e, itemId) => {
const update = [...productProperties];
const i = update.findIndex((group) => group.id === itemId);
if (i !== -1) {
update[i].name = {
...update[i].name,
[selectedLocale]: e.currentTarget.value,
};
setProductProperties([...update]);
}
}
The state of originalProductProperties also updates. Why? setOriginalProductProperties is never called here, I am also not mutating any state directly and I use the spread operator to be sure to create new references. I am lost.
Preface: It sounds like the two arrays are sharing the same objects. That's fine provided you handle updates correctly.
Although you're copying the array, you're modifying the object in the array directly. That's breaking the main rule of state: Do Not Modify State Directly
Instead, make a copy of the object as well:
(e, itemId) => {
const update = [...productProperties];
const i = update.findIndex((group) => group.id === itemId);
if (i !== -1) {
update[i] = { // *** Note making a new object
...update[i],
[selectedLocale]: e.currentTarget.value,
};;
setProductProperties(update); // (No need to *re*copy the array here, you've already done it at the top of the function)
}
}
Or, since you have that i !== -1 check there, we could copy the array later so we don't copy it if we don't find the group matching itemId:
(e, itemId) => {
const i = productProperties.findIndex((group) => group.id === itemId);
if (i !== -1) {
const update = [...productProperties];
update[i] = { // *** Note making a new object
...update[i],
[selectedLocale]: e.currentTarget.value,
};;
setProductProperties(update);
}
}
FWIW, in cases where you know there will be a match, map is good for this (but probably not in this case, since you seem to indicate the group may not be there):
(e, itemId) => {
const update = productProperties.map((group) => {
if (group.id === itemId) {
// It's the one we want, create the replacement
group = {
...group,
[selectedLocale]: e.currentTarget.value,
};
}
return group;
});
setProductProperties(update);
}
Or sometimes you see it written with a conditional operator:
(e, itemId) => {
const update = productProperties.map((group) =>
group.id === itemId
? { // It's the one we want, create a replacement
...group,
[selectedLocale]: e.currentTarget.value,
}
: group
);
setProductProperties(update);
}
I have the following in componentDidMount
componentDidMount() {
const company = this.props.location.state.company;
const financials = this.props.location.state.financials;
let { values } = this.state;
values = EDITABLES.map((data) => { //EDITABLES is a const imported array
return {
id: data.id,
name: data.name,
value: financials[data.id]
newValue: "",
};
});
this.setState({
values,
});
}
However, if I console.log the values at the time of render, the first console log shows it as undefined, but the next one shows it as defined. I am also able to map the values in my render method without any problem.
render() {
const {
company,
financials,
values,
} = this.state;
console.log(values, "check")
My problem is that in my render method, I call a function {this.calculate(financial.id)}
calculate(financial) {
const { financials, values } = this.state;
console.log(values, "values");
let numerator;
if (financial === "tev_profit") { //this line is okay
let tev = values.find(o => o.id === "total_value");
console.log(tev, "here");
numerator = tev.newValue; //this line is causing error.
From the console log it seems as if tev is sometimes defined but other times not. I'm not sure where I'm going wrong because I also added this to my code but still facing the same typeError:
this.calculate = this.calculate.bind(this);
Update:
I have tried adding if(values) to my calculate function before I go into any other if block, but getting the same error
Issue
Array.prototype.find can return undefined if element isn't found.
The find() method returns the value of the first element in the
provided array that satisfies the provided testing function. If no
values satisfy the testing function, undefined is returned.
Solution
Check for valid tev object before accessing properties
if (financial === "tev_over_ltm_gross_profit") {
const tev = values.find(o => o.id === "total_enterprise_value");
if (tev) {
// tev exists
numerator = tev.newValue;
}
I have a simple component that allows me to select an item from a list, then remove an item from a list. I display the active list within a parent component. No matter what I do or how I approach it, the removal of an active component is never updated unless they are all in active.
Here is a smaller (yet large snippet) of how it is setup. Below it I describe where I found to be the problem:
const Viewer = () => {
const [items, setItems] = useState(["inactive"]);
return (
<ItemSelect setItems={setItems} selected={items}/>
<DisplayItems items={items}/>
)
}
const ItemSelect = ({setItems, selected}) => {
const handleActiveItems = (activeItems) => {
setItems(activeItems);
}
return (
<SelectItems
handleActiveItems={handleActiveItems}
items={selected}
/>
)
}
const SelectItems = ({handleActiveItems, items}) => {
const [selected, setSelected] = useState([])
useEffect(() => {
setSelected(items);
}, [items]);
const randomTestItem = ["apple", "peach", "orange"];
const handleOnClick = (isSelected, item) => {
let tmpItems = items;
if (isSelected) {
let index = tmpItems.indexOf("inactive");
if (index > -1) {
handleActiveItems([option]);
} else {
handleActiveItems([...selected, option]);
}
} else if (!isSelected) {
let index = tmpItems.indexOf(option);
if (index > -1) {
tmpItems.splice(index, 1);
if (tmpItems.length === 0) {
handleActiveItems(["inactive"]);
} else {
handleActiveItems([tmpItems]);
}
}
}
}
return (
{
randomTestItem?.map((item,index) => {
return (
<DisplayClickable item={item} onClick={handleOnClick} key={index}/>
)
})
}
)
}
<DisplayClickable item={item} onClick={handleOnClick}/> holds a useState() that toggle from active/inactive.
I've tested this in many different area's I believe the crux of the problem to be here:
} else if (!isSelected) {
let index = tmpItems.indexOf(option);
if (index > -1) {
tmpItems.splice(index, 1);
if (tmpItems.length === 0) {
handleActiveItems(["inactive"]);
} else {
handleActiveItems([tmpItems]);
}
}
}
specifically:
} else {
handleActiveItems([tmpItems]);
}
When I unselect all the items and switch the array back to "inactive", everything updates instantly and exactly how you would expect. Selecting items always adds to the list correctly, it's removing them that everything goes wonky. I've done a console.log right before calling handleActiveItems() and the tmpItems array is always correct to what it should be. It just never updates the set state.
Within handleActiveItems the log also shows it is receiving the array just before setting it. It just never sets it.
I believe since you are using the splice method, you just modify the existing array and React does not recognize it as "updatable". You can try to use the filter method:
if (index > -1) {
const newArray = tmpItems.filter((_, itemIndex)=> itemIndex !== index)
if (newArray.length === 0) {
handleActiveItems(["inactive"]);
} else {
handleActiveItems(newArray);
}
}
With the code above, filter method will generate a new array.
Give it a try, hopefully it will help =)
update
I've just realized, maybe you don't need the extra [] you are putting into handleActiveItems(). So instead of:
handleActiveItems([tmpItems])
It could be just:
handleActiveItems(tmpItems)
I figured it out.
It all came down to this line:
let tmpItems = items;
Changing to this:
let tmpItems = [...items];
for some reason allowed React to pay more attention and notice that there was in fact a change.
I just changed in my development build and it works without a hiccup.
I have the array of strings and i need to filter this array due to the input value. I filter it through filter function, but when i clear input the array stays filtered. How get an initial array?
filterCoins = (e) => {
let updatedList = this.state.data;
updatedList = updatedList.filter(function(item){
return item.toLowerCase().search(
e.target.value.toLowerCase()) !== -1;
});
this.setState({data: updatedList});
};
Solution depends on how you're getting the initial data.
If it's from props, better have it into state for populating filteredList.
updatedList = this.props.data.filter(function(item){
return item.toLowerCase().search(
e.target.value.toLowerCase()) !== -1;
});
If it's self contained state component, then use class variable this.data to store initial data filter the searched item from this.data;
updatedList = this.data.filter(function(item){
return item.toLowerCase().search(
e.target.value.toLowerCase()) !== -1;
});
this.setState({data: updatedList});
So the suggestion is always filter from the original list.
The original data is currently being replaced in state whenever the input value is changed.
This means you either need to keep the old data around in state so that you can revert back to it when input is cleared or come upon another way to do the implementation.
I would set the input value in state in place of updating data when the input value changes.
setCoinFilter = (e) => {
this.setState({ filterText: e.target.value });
}
Then in the component rendering the items; the filter operation can be performed.
state = {
data: [],
filterText: '',
}
filterCoins(coins, filterText) {
return coins.filter(function(coin) {
return coin.toLowerCase(filterText.toLowerCase()).search() !== -1;
});
}
shouldNotApplyFilter() {
const { filterText } = this.state;
return filterText === null || filterText === '';
}
render() {
const data = this.shouldNotApplyFilter() ? this.state.data :
filterCoins(this.state.data, this.state.filterText)
// render data
}
when your input is cleared, e.target.value will be empty, then you need to reset this.state.data to its original value. I assumed the original value is kept in the state, so you can write the filtercoins function like this
filterCoins = (e) => {
let newData;
if (e.target.value) {
newData = this.state.data.filter(item =>
item.toLowerCase().indexOf(e.target.value.toLowerCase()) !== -1
);
} else {
newData = this.state.originalData;
}
this.setState({data: newData});
};
I understand that it can be bad to reassign function parameters but I don't quite see how it would be done in this case? I'm using a forEach loop to cycle through the todo list array (which is on an object) todos and alter the completed property and I don't see how I can not reuse eachTodo
How would this be rewritten so that it has the same functionality but doesn't reuse eachTodo?
this.todos.forEach((eachTodo) => {
if (completedTodos === totalTodos) {
eachTodo.completed = false;
} else {
eachTodo.completed = true;
}
});
Full project here
You are not reassigning parameters here. If you were reassigning them, there would be some line with eachTodo = in it - but that's not the case here. Rather, you're mutating the eachTodo parameter.
If you want to avoid mutating the parameter as well, one option would be to use .map to create a copy of each eachTodo, and then reassign this.todos outside of the forEach call:
this.todos = this.todos.map((eachTodo) => {
if (completedTodos === totalTodos) {
return { ...eachTodo, completed: false };
} else {
return { ...eachTodo, completed: true };
}
});
(make sure there are no other references to individual todos to avoid memory leaks)
Your code doesn't assign to eachTodo, so I don't see how the link to the discussion about reassigning parameters is relevant.
What do you mean by "reuse eachTodo"? If you mean you want code that mentions the variable name less often, here's one way:
if (completedTodos === totalTodos) {
eachTodo.completed = false;
} else {
eachTodo.completed = true;
}
can be reduced (by pulling out the common eachTodo.completed = part) to:
eachTodo.completed = completedTodos === totalTodos ? false : true;
This line can be simplified further (as a general rule, whenever you have a ?: operator where one of the branches is just true or false, it can be simplified):
eachTodo.completed = completedTodos !== totalTodos;