not re-rendering on setState - reactjs

I have a condition given below where I want to update an array of objects present in state, although the state is changing but not rendering, maybe somehow the state is being directly mutated, or any other reason. I'am unable to figure out what am I doing wrong
handleCondition = e => {
const { value, id } = e.target;
this.setState(
({ newData }) => {
const testData = newData;
testData.find(data => data.variable === id.split('_')[1])[id.split('_')[0]] = value;
return ({
newData: testData
});
},
() =>
localStorage.setItem(
"newData",
JSON.stringify(this.state.newData)
)
);
};
render(){
return(
<select
id={`wall_${data.variable}`}
className="custom-slt-home"
onChange={this.handleCondition}
value={data.wall}
>
<option value="paint">Paint</option>
<option value="wallpaper">Wallpaper</option>
</select>
)

In this line:
testData.find(data => data.variable === id.split('_')[1])[id.split('_')[0]] = value;
You don't mutate an array, so as a result you're returning to the setState the same value.
Try this instead:
const itemIndex = newData.findInex(data => data.variable === id.split('_')[1]);
const item = [...newData[itemIndex]];
item[id.split('_')[0] = value;
return ({
newData: [...newData.slice(0, itemIndex), item, ...newData.slice(itemIndex + 1)];
});
There may be some issues, because you didn't provide any info about your data structure. But I hope you'll understand the point.

Related

Next/React - How to correctly build up state from multiple child components

I think this is easier to explain using a codesandbox link. This follows on from a previous question of mine, which may help provide more overall context. Currently, when interacting with the child elements (i.e. inputs), the state updates to {"values":{"0":{"Text1":"test"},"1":{"bool":true}}}. The issue is that if you interact with the other inputs within a Parent component, e.g. Text2 in the Parent component with id 0, it will overwrite the value already in the state, which makes it look like this {"values":{"0":{"Text2":"test"},"1":{"bool":true}}}. I want it to look like {"values":{"0":{"Text1":"test", "Text2":"test"},"1":{"bool":true}}}.
This is my try with your problem. I would like to have childIndex instead of number like you. It would be easier to work with other components later.
Here is my codesandbox
import { useEffect, useState } from "react"
import Parent from "./Parent"
const id1 = 0
const id2 = 1
interface Boo {
childIndex: number
value: {
[name: string]: string | boolean
}
}
const GrandParent: React.FC = () => {
const [values, setValues] = useState<Boo[]>([])
const valuesChange = (e: React.ChangeEvent<HTMLInputElement>, id: number) => {
console.log("change event")
const name = e.target.name
let value: any
if (name === "bool") {
value = e.target.checked
} else {
value = e.target.value
}
setValues((prev) => {
// Update new value to values state if input already there
let updateBoo = prev.find((boo) => boo.childIndex === id)
if (updateBoo) {
// Update Values
const valKeys = Object.keys(updateBoo.value)
const valIndex = valKeys.find((val) => val === name)
if (valIndex) {
updateBoo.value[valIndex] = value
} else {
updateBoo.value = { ...updateBoo.value, [name]: value }
}
} else {
// Create new if not added
updateBoo = {
childIndex: id,
value: { [name]: value }
}
}
return [...prev.filter((boo) => boo.childIndex !== id), updateBoo]
})
}
useEffect(() => {
console.log("Render", { values })
})
return (
<>
<div>{JSON.stringify({ values }, undefined, 4)}</div>
<br />
<br />
<Parent valueChange={(e) => valuesChange(e, id1)} key={id1} />
<Parent valueChange={(e) => valuesChange(e, id2)} key={id2} />
</>
)
}
export default GrandParent
The trick is you should return the previous state of the property too:
setValues((prev) => {
return {
...prev,
[id]: {
...(prev && (prev[id] as {})), // <= return the previous property state
[name]: value
}
}
})
I'm not very good at typescript but I tried my best to solve some types' errors
you can see an example below

Access latest state value in the same function that is used to set the state

my state
const [selectedRows, setSelected] = useState([])
const handleRowClick = useCallback((id) => {
if(selectedRows.includes[id]) {
arr = selectedRows.filter((item) => item !== id) // <==Trying to access the state here
setSelected((prev) => [arr])
} else {
setSelected((prev) => [id])
}
})
Everytime I try to access the selectedRows inside the handleRowClick function it just returns its default value,ie. an empty array
You aren't using a dependency array for useCallback. You probably want this:
const handleRowClick = useCallback((id) => {
if(selectedRows.includes[id]) {
arr = selectedRows.filter((item) => item !== id) // <==Trying to access the state here
setSelected((prev) => [...arr])
} else {
setSelected((prev) => [...id])
}
}, [selectedRows])
Note: notice that I'm destructuring the array so you get a new copy of the array and not an array with just one element.

react state not updating on input change

I have an array of objects in a useState hook with which i rendered input elements.
The state doesn't get updated when the input element changes.
Here Is My Code.
ProceduralResponseAnswerCreate Component
const ProceduralResponseAnswerCreate = () => {
const [proceduralAnswers, setProceduralAnswers] = useState([{ "value": "One" }, { "value": "two" }])
return <>
<ol>
{proceduralAnswers.map((answer, answer_index) => <input key={answer_index} style={inputStyle} onChange={(event) => updateAnswerValue({ event, setProceduralAnswers, answer_index })} value={answer.value} />)}
</ol>
</>
}
export default ProceduralResponseAnswerCreate
updateAnswerValue function
export const updateAnswerValue = ({ event, setProceduralAnswers, answer_index }) => {
setProceduralAnswers(state => {
var newState = state;
var currentAnswer = newState.filter((state_1, index) => index === answer_index)[0]
currentAnswer.value = event.target.value
return newState
})
}
I think it doesn't work because the next state has the same Reference as the previous state, you only change the value of one element. The useState hook will trigger a rerender, if the reference changes. So your Callback must look like this.
setProceduralAnswers(state => {
let newState = state;
let currentAnswer = newState.filter((state_1, index) => index === answer_index)[0]
currentAnswer.value = event.target.value
return [...newState]
})
try this
export const updateAnswerValue = ({ event, setProceduralAnswers, answer_index }) => {
setProceduralAnswers(state => {
var newState = state;
newState[answer_index].value = event.target.value
return newState
})
}
You are returning the newState instead of the currentAnswer.
First off, if you're only trying to find one item use [].find not filter.
var newState = state; does not give you a fresh copy, change newState, and you'll be changing state too, you need to do var newState = [...state]; (warning: this is a shallow copy only).
It looks like you think currentAnswer shares a reference with newState in some way, which it doesn't. Whenever you run map, find, filter or reduce you're getting a fresh copy of the array so there will not be a link between the 2.
A good bet here is to just map the state
export const updateAnswerValue = ({
event,
setProceduralAnswers,
answer_index
}) => {
setProceduralAnswers((state) => state.map((state_1, index) => {
if (index === answer_index) {
return {
...state_1,
value: event.target.value
}
}
return state_1;
}));
};

SetState is not changing the state value

console.log for item.completion is triggered twice. It displays the new inverted value and switch back to its original value. So, every time toggleTask function is called the item.completion is not changed. What causes this?
toggleTask(id) {
this.setState(prevState => {
const newTodos = prevState.todos.map(item => {
if (item.id === id) {
item.completion = !item.completion
console.log(item.completion)
}
return item
})
return { todos: newTodos }
})
}
The problem is here,
this.setState(prevState => {
const newTodos = prevState.todos.map(item => {
if (item.id === id) {
item.completion = !item.completion // !! HERE !!
console.log(item.completion)
}
return item
})
return { todos: newTodos }
})
AS, Object are passed by reference not by value, so when your modifying it the state is getting directly modified and React sees no diff from your return and the state, thus no render takes place.
try something that doesn't mod the object directly, like this,
this.setState(prevState => {
const newTodos = prevState.todos.map(_item => {
const item = Object.create(_item); // NEW
if (item.id === id) {
item.completion = !item.completion
console.log(item.completion)
}
return item
})
return { todos: newTodos }
})
inspired from JavaScript: How to pass object by value?

Why is this throwing a TypeError?

I have a react component that is supposed to map a 'persons' state and return a 'person' list, it displays the list fine, and deletePersonHandler still works, but when I begin typing in an input it throws a TypeError.
TypeError: props.persons.map is not a function
I'm quite new to JS and very new to React so any pointers would be much appreciated!
My persons component:
const persons = (props) =>
props.persons.map((person, index) => {
return (
<Person
click={() => props.clicked(index)}
name={person.name}
age={person.age}
//each item in a list needs a key, should be unique and unchanging
key={person.id}
changed={(event) => props.changed(event, person.id)} />
)
});
Then in App.js I have
let people = null;
if (this.state.showPersons) {
people = (
<div>
<Persons
persons = {this.state.persons}
clicked = {this.deletePersonHandler}
changed = {this.nameChangedHandler}
/>
</div>
);
}
And finally I have my nameChangedHandler also in App.js
nameChangedHandler = (event, id) => {
const personIndex = this.state.persons.find(p => {
return p.id === id;
});
const person = {...this.state.persons[personIndex]};
person.name = event.target.value;
person[personIndex] = person;
this.setState({ persons: person });
}
I'm expecting the name to change as you type, however as soon as you start typing the typeerror is thrown
Many thanks!
It is because persons is an object. and .map is a function unique to Arrays
instead of props.persons.map try something like Object.keys(props.persons).map
doc for Object.keys() is here https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
Hope that helped (:
You are setting persons to an object. From the code, I can only conclude that persons is an array initially (since you're able to call find on it). You can change your nameChangeHandler to
nameChangedHandler = (event, id) => {
const personIndex = this.state.persons.find(p => {
return p.id === id;
});
const person = { ...this.state.persons[personIndex] };
person.name = event.target.value;
person[personIndex] = person;
// Notice the change here
const newPersons = [...this.state.persons].splice(personIndex, 1, person);
this.setState({ persons: newPersons });
};
I use Array#splice to replace the old person and generate a new array.

Resources