React with lists in state, how to set new state? - javascript

I ran into an issue with updating part of the state that is a list that's passed on to children of a component.
I pass in a list to a child, but then have trouble to update that list and have the child reflect the new state;
<ItemsClass items={this.state.items1} />
When I change the value of this.state.items1, the component doesn't render with the new value.
this.setState({items1: []}); // this has no effect
However, if I change the already existing array (not replacing it new a new empty one), the component renders as I wish;
this.setState(state => { clearArray(state.items1); return state; });
That means the state updating function isn't pure, which React states it should be.
The HTML;
<div id='app'></div>
The js;
class ItemsClass extends React.Component {
constructor(props){
super(props);
this.state = {items: props.items};
}
render() {
var items = this.state.items.map(it => <div key={it.id}>{it.text}</div>);
return(
<div>{items}</div>
);
}
}
function ItemsFunction(props) {
var items = props.items.map(it => <div key={it.id}>{it.text}</div>);
return(
<div>{items}</div>
);
}
class App extends React.Component {
constructor(props){
super(props);
var items = [{id:1, text: 'item 1'}, {id: 2, text: 'item 2'}];
this.state = {
items1: items.slice(),
items2: items.slice(),
items3: items.slice()
};
this.clearLists = this.clearLists.bind(this);
}
clearLists() {
// for items1 and items2, clear the lists by assigning new empty arrays (pure).
this.setState({items1: [], items2: []});
// for items3, change the already existing array (non-pure).
this.setState(state => {
while (state.items3.length) {
state.items3.pop();
}
})
}
render() {
return (
<div>
<button onClick={this.clearLists}>Clear all lists</button>
<h2>Items rendered by class, set list to new empty array</h2>
<ItemsClass items={this.state.items1} />
<h2>Items rendered by class, empty the already existing array</h2>
<ItemsClass items={this.state.items3} />
<h2>Items rendered by function</h2>
<ItemsFunction items={this.state.items2} />
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('app'));
Try it out on codepen.
It seems that the ItemsClass doesn't update even though it's created with <ItemsClass items={this.state.items1}/> and this.state.items1 in the parent changes.
Is this the expected behavior? How can I update the state in the ItemsClass child from the parent?
I'm I missing something? This behavior seems quite error prone, since it's easy to assume that the child should follow the new state, the way it was passed in when the child was created.

You're copying the props of ItemsClass into the state when the component gets initialized - you don't reset the state when the props change, so your component's updates don't get displayed. To quote the docs:
Beware of this pattern, as state won't be up-to-date with any props update. Instead of syncing props to state, you often want to lift the state up.
If your component has to do something when the props change, you can use the componentWillReceieveProps lifecycle hook to do so (note that it doesn't get run when the component initially mounts, only on subsequent prop updates).
That said, there's zero reason for you to be duplicating the props here (and honestly there's rarely a good reason to do so in general) - just use the props directly, as you're doing with ItemsFunction, and everything will stay in sync:
class ItemsClass extends React.Component {
render() {
var items = this.props.items.map(it => <div key={it.id}>{it.text}</div>);
return(
<div>{items}</div>
);
}
}
Here's a working version of your Codepen: http://codepen.io/anon/pen/JNzBPV

Related

shouldComponentUpdate() is not being called

Problem
I've parent class which contains list of items and renders component for each item of the list. When some item has been changed (even only one), all items in the list are being rerendered.
So I've tried to implement shouldComponentUpdate(). I am using console.log() to see if it is called but I can't see the log. I've found question shouldComponentUpdate is not never called and tried to return return (JSON.stringify(this.props) !=JSON.stringify(nextProps)); but component still renders itself again. So I've tried just to return false (like do not ever update) but it still does. As the last try I've used PureComponent but it is still being rerendered.
Question
How can I stop children re-rendering if the parent list changes and why is ShouldComponentUpdate never called?
Edit
I've noticed something what I didn't mention in question, I'm sorry for that. I am using context. If I don't use context -> it's ok. Is there any chance to stop re-render while using context? (I'm not using context on updated item - values of context didn't change).
Example
I've parent class which iterates list and renders TaskPreview component for each item of list:
class Dashboard extends React.Component
{
constructor(props) {
this.state = {
tasks: {},
};
}
onTaskUpdate=(task)=>
this.setState(prevState => ({
tasks: {...prevState.tasks, [task._id]: task}
}));
// ... some code
render() {
return (
<div>
{(!Object.entries(this.props.tasks).length)
? null
: this.props.tasks.map((task,index) =>
<TaskPreview key={task._id} task={task} onChange={this.onTaskUpdate}/>
})}
</div>
)
}
}
and I've children TaskPreview class:
class TaskPreview extends React.Component
{
shouldComponentUpdate(nextProps) {
console.log('This log is never shown in console');
return false; // just never!
}
render() {
console.log('task rendered:',this.props.task._id); // indicates rerender
return(<div>Something from props</div>);
}
}
TaskPreview.contextType = TasksContext;
export default TaskPreview;
As #Nicolae Maties suggested I've tried to use Object.keys for iteration instead of direct map but it still doesn't call "shouldComponentUpdate" and still being re-rendered even if there is no changes.
Updated code:
render() {
return (
<div>
{(!Object.entries(this.props.tasks).length)
? null
: Object.keys(this.props.tasks).map((key,index) => {
let task = this.props.tasks[key];
<TaskPreview key={task._id} task={task}/>
}
})}
</div>
)
}
Component is being re-rendered because of .contextType.
TaskPreview.contextType = TasksContext;
Also as is mentioned in documentation:
The propagation from Provider to its descendant consumers (including .contextType and useContext) is not subject to the shouldComponentUpdate method, so the consumer is updated even when an ancestor component skips an update. Source: reactjs.org/docs/context
You have to use context somehow else or do not use it at all.
You can use Context.Consumer which won't force re-render of current component but it might force re-render of its children.
<TasksContext.Consumer>
{value => /* render something based on the context value */}
</TasksContext.Consumer>
Instead of return (JSON.stringify(this.props) != JSON.stringify(nextProps)); in your shouldComponentUpdate() life cycle, try specifying tasks object like this return (JSON.stringify(this.props.tasks) != JSON.stringify(nextProps.tasks));
Maybe react is creating new instances of your component and replaces the old instances with them. That's why you're probably not getting your lifecycle method invoked. That can happen if the key property you're assigning in the map always changes.
use from pureComponent and array as state:
class Dashboard extends React.PureComponent
{
constructor(props) {
this.state = {
tasks: this.props.tasks
}
}
onTaskUpdate=(task)=>
this.setState(prevState => ({
tasks: [...prevState.tasks, task] // render only new task
}));
render() {
const {tasks} = this.state
return (
<div>
{tasks.map(task => <TaskPreview key={task._id} task={task} />)}
</div>
)
}
}
class TaskPreview extends React.PureComponent
{
render() {
console.log('task rendered:',this.props.task._id); // indicates rerender
return(<div>Something from props</div>);
}
}
In the shouldComponentUpdate() method of your TaskPreview component, you should check if the next props have changes in comparison to the current props. Then if there are changes, return true to update the component, otherwise false.
The following example compares all the fields of props object with the new props object. But you can only check the props you are interested in.
shouldComponentUpdate(nextProps) {
return !!(Object.keys(nextProps).find(key => nextProps[key] !== this.props[key]));
}
I tried with below code snippet, shouldComponentUpdate worked as I expected. Could you share your Dashboard initial props ?
class Dashboard extends React.Component {
constructor(props) {
this.state = {
tasks: {}
};
}
onTaskUpdate = task =>
this.setState(prevState => ({
tasks: { ...prevState.tasks, [task._id]: task }
}));
// ... some code
render() {
return (
<div>
{!Object.entries(this.props.tasks).length
? null
: Object.keys(this.props.tasks).map((key, index) => {
let task = this.props.tasks[key];
return (
<TaskPreview
key={task._id}
task={task}
onChange={this.onTaskUpdate}
/>
);
})}
</div>
);
}
}
class TaskPreview extends React.Component {
shouldComponentUpdate(nextProps) {
console.log("This log is never shown in console");
return nextProps.task._id != this.props.task._id;
}
render() {
console.log("task rendered:", this.props.task); // indicates rerender
return (
<button onClick={() => this.props.onChange(this.props.task)}>
Something from props
</button>
);
}
}
my initial props for Dashboard component is :
<Dashboard tasks={{test:{_id:'myId', description:'some description'}}}/>

setState not re-rendering react component inside array

I'm trying to create a react app that adds a react component when pressing a button a then re-renders it. I'm using an array of p elements inside state as a test. The event handler function uses setState to modify the array but for some reason it does not re-renders de component so the change (the new element) is not showed in the screen. What I'm doing wrong?
import React, {Component} from "react";
export default class Agenda extends React.Component{
constructor(props){
super(props);
this.list = [];
this.state = {
list:[]
};
this.addItem = this.addItem.bind(this);
const items = ["Hola Mundo 1","Hola Mundo 2","Hola Mundo 3"];
const itemComponent = items.map((item) =>
<p>{item}</p>
);
this.list = itemComponent;
this.state.list = itemComponent;
}
addItem(){
//Adds a new paragraph element at the end of the array
this.list[3]= <p>Hola Mundo 4</p>;
this.setState(
{
list: this.list
}
);
}
render(){
return(
<div id={this.props.id} className={this.props.className}>
{this.state.list}
<button onClick={this.addItem}>
Add item
</button>
</div>
);
}
}
There are several things you are doing which are bad practice in react. One or more of them are likely causing your problem. The issues are:
1) Don't save components in state. Your state should be just the minimum data that determines the components, and the components themselves show up in render. By storing components in state you make it easy to forget to update the state, and thus cause the rendering to not change.
2) Don't duplicate state as instance variables. By doing this, you're only forcing yourself to manually keep two pieces of data in sync with eachother. Instead, have a single source of truth, and have everything else derive from that.
3) Don't mutate state. If you want to add items to an array, create a new array which is a shallow copy of the old one, then append to that new array. React relies on state being immutable to tell whether state has changed, so mutations are an easy way to accidentally make rerendering not happen.
export default class Agenda extends React.Component {
constructor(props) {
super(props);
this.state = {
list: ['Hola Mundo 1', 'Hola Mundo 2', 'Hola Mundo 3'],
};
this.addItem = this.addItem.bind(this);
}
addItem() {
this.setState((oldState) => {
const newList = [...oldState.list];
newList.push('Hola Mundo 4');
return { list: newList };
});
}
render() {
return (
<div id={this.props.id} className={this.props.className}>
{this.state.list.map(item =>
<p>{item}</p>
)}
<button onClick={this.addItem}>
Add item
</button>
</div>
);
}
}
this.list[3]= <p>Hola Mundo 4</p>;
The above statement does not change the reference of the list variable. So set state doesn't see a difference while rendering. Instead do something like this -
this.setState({
list: this.list.concat(<p>Hola Mundo 4</p>)
});

Make a copy of state to edit it indirectly in children

I'm setting state in the parent component, then passing it as props to a child component. In the child, I would like to make a copy of that state object (received through props) to edit without updating the original state object yet. I will then display that information in the current child component and send the updated state object back to the parent to update the original state object. The now updated parent state would then be used to display information in other child elements.
In child component - where 'this.props.shifts' is the passed down state from the parent:
this.dowCopy = { ...this.props.dow };
this.state = {
dowCopy: this.dowCopy
}
addTempShift = (day) => {
const emptyShift = {arbitraryData};
const dowCopyCopy = {...this.state.dowCopy};
dowCopyCopy[day].shifts.push(emptyShift);
this.setState({ dowCopy: dowCopyCopy })
}
Everything works as intended, but when I set state here in the child component, it's also updating state in the parent. It was my understanding that the spread operator took a copy of the state object. What am I missing?
Spread syntax creates shallow copies as told by #Timothy Wilburn. So, if you change copied object property directly then you will mutate the original one. You can search for "deep copy" or you can again use spread syntax in the child component.
class App extends React.Component {
state = {
dow: {
monday: {
shifts: [{ name: "foo" }, { name: "bar" }],
},
}
}
render() {
return (
<div>
In parent: {JSON.stringify(this.state.dow)}
<Child dow={this.state.dow} />
</div>
);
}
}
class Child extends React.Component {
state = {
dowCopy: { ...this.props.dow }
}
addTempShift = (day) => {
const emptyShift = { name: "baz" };
this.setState( prevState => (
{
dowCopy: { ...prevState.dowCopy, [day]: { shifts:
[...prevState.dowCopy[day].shifts, emptyShift] } }
}
))
}
componentDidMount() {
this.addTempShift( "monday");
}
render() {
return (
<div>In Child: { JSON.stringify(this.state.dowCopy)}</div>
)
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Oh, boy.
But, I agree with #George Chanturidze, you are overdoing. Do not copy the state like that. If you want to show some changed data in the Child component, then change it using this state as its prop. Then, if you want to change the parent's state, gather this data and send back to the parent with a callback and set the state there.

Child Component(stateful) not understanding parent components index changed

I am new to ReactJS and really looking forward for your help here. I created a prototype of my doubt. I have an array of Objects which is my State in my parent component(Application) and i am mapping them to a child component(List) and this child component is a stateful component which changes its state only for its visual perspective(that means i don't want to update that state in my parent component, of course this issue is solved if i update that as well in my parent's state). So if i update the state of my child and add a new data to the parents state the component re renders and my child components index is misplaced by child's state.
class List extends React.Component {
constructor() {
super();
this.state = {
checked:false
}
}
onChange = () => {
this.setState({checked:!this.state.checked})
}
render() {
const {data} = this.props;
return(
<tr>
<td>{data.name}</td>
<td>{data.age}</td>
<td><input type='checkbox' onChange={this.onChange} checked={this.state.checked?true:false}/></td>
</tr>
)
}
}
class AddData extends React.Component {
...
}
class Application extends React.Component {
constructor() {
super();
this.state = {
datas: [
{name:'user1',age:'26'},{name:'user2',age:'27'}
]
}
}
addData = (data) => {
let newData = [data];
this.setState((prevState) => ({datas:[...newData,...prevState.datas]}));
}
render() {
const { datas } = this.state;
return (
<div>
<h3>Example</h3>
<table className="table table-bordered table-striped">
{datas.map((data,index) =>
<List data={data} key={index}/>
)}
</table>
<AddData addData={this.addData}/>
</div>
)
}
}
So lets say in the above example,
I checked the checkbox for user1
Then i add a new user and update my state
The new user is stacked above current state data
My component re-renders and now the checkbox will be checked for the new user.
So is there any way to let my child state to know the parent has changed, please find the working example here
Thanks in Advance!
it happens exactly because you are using index for key, react uses keys to remember which component was changed, so in your example if you check very first element its key will be 0 (as its index in array), so now react knows that your element with key 0 was checked, when you re-render new element receives key 0 and react applies changes to this element cause it thinks this was your original element that you checked. use original properties from object and it will work fine, if you don't have such properties try to assign id.. uuid is a great library for it. or you can add new data as last element of the array, opposed to what you are doing now, but its not a recommended solution.

Splice() method not works

I have some problem with splice() method in my React.js app.
So, this is an example app. Deletion not works now. What's wrong here? Part of code:
class CardList extends React.Component {
static propTypes = {
students: React.PropTypes.array.isRequired
};
// ADD DELETE FUNCTION
deletePerson(person) {
this.props.students.splice(this.props.students.indexOf(person), 1)
this.setState()
}
render() {
let that = this
return <div id='list'>
{this.props.students.map((person) => {
return <Card
onClick={that.deletePerson.bind(null, person)}
name={person.name}>
</Card>
})}
</div>
}
}
class Card extends React.Component {
render() {
return <div className='card'>
<p>{this.props.name}</p>
{/* ADD DELETE BUTTON */}
<button onClick={this.props.onClick}>Delete</button>
</div>
}
}
http://codepen.io/azat-io/pen/Vaxyjv
Your problem is that when you call
onClick={that.deletePerson.bind(null, person)}
You bind this value to null. So inside of your deletePerson function this is null instead of actual component. You should change it to
onClick={that.deletePerson.bind(this, person)}
And everything would work as expected =)
Changing the bind value to this will definitely cause the call to this.setState() to work, thus triggering the re-render, however I strongly recommend against the approach you've taken.
Props are supposed to be immutable. Instead use internal state and replace with new values rather than mutate them. To do this, set the state of your component in the constructor by doing something like:
constructor(props) {
super(props)
this.state = {
students: ...this.props.students
}
}
And now when you need to delete a person:
deletePerson(person) {
// notice the use of slice vs splice
var newStudents = this.props.students.slice(this.props.students.indexOf(person), 1)
this.setState({ students: newStudents })
}
And finally use this.state.students in your render method instead.
The reasoning behind this is that props are passed directly from the parent container component so modifying them wouldn't really make sense. To make more sense of my own code, I tend to pass in the prop named initialStudents and set my state to students: ...initialStudents to ensure I make the distinction between my prop variable and my state variable.

Categories

Resources