setState not re-rendering react component inside array - javascript

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>)
});

Related

Component doesn't re-render when store updates - Mobx

I have three components; Form, Preview & AppStore. Clicking a button in Form adds an item to the store. This seems to work fine except that the list in the Preview component isn't updating/re-rendering when the store changes even though it has an #observer decorator. What am I missing?
Form has a button and handler function that adds an item to the store:
#inject('AppStore')
#observer class Form extends React.Component{
handleAddItem= (item) =>{
const {AppStore} = this.props;
AppStore.addItem(item);
console.log(AppStore.current.items)
}
render(){
return(
<button onClick={() => this.handleAddItem('Another Item')}>Add Item</button>
)}
}
Preview maps through the items (I'm using a drag and drop hoc so my code might look a bit odd)
#inject('AppStore')
#observer class Preview extends React.Component
...
return(
<ul>
{items.map((value, index) => (
<SortableItem key={`item-${index}`} index={index} value={value} />
))}
</ul>)
...
return <SortableList items={AppStore.current.items} onSortEnd={this.onSortEnd} />;
Here is store:
import { observable, action, computed } from "mobx";
class AppStore {
#observable other = {name: '', desc:'', items: [ 'item 1', 'item 2', 'item 3'], id:''}
#observable current = {name: '', desc:'', items: [ 'item 1', 'item 2', 'item 3'], id:''}
#action addItem = (item) => {
this.current.items.push(item)
}
}
const store = new AppStore();
export default store;
I'm fairly certain this is a case where MobX doesn't extend observability all the way down into your current.items array.
Objects in MobX will extend observability when first constructed/initialized -- so current.items is an observable property in the sense that if you changed it's value to some other primitive, your component would re-render.
For example:
current.items = 1; // changing it from your array to some totally new value
current.items = []; // this _might_ also work because it's a new array
Similarly, if AppStore had a top-level observable items that you were changing, then calling items.push() would also work.
class AppStore {
#observable items = [];
#action additem = (item) => {
this.items.push(item);
}
}
The problem in your case is that items is buried one level deep inside an observable object -- so pushing items into the current.items array isn't changing the value of the property in a way that MobX can detect.
It's admittedly very confusing, and the common MobX pitfalls are sometimes hard to understand.
See also this line in the Object documentation:
Only plain objects will be made observable. For non-plain objects it
is considered the responsibility of the constructor to initialize the
observable properties.
Try to replace your action to be:
#action addItem = (item) => {
this.current.items = this.current.items.concat([item]);
}
Here instead of using push for mutating the property, use concat which is used to merge two arrays and return a whole new array with a new reference that MobX can react to.

How high should state be lifted in React?

Should intermediate components control parts of state and call props passed to them or should state be lifted higher? I've been going back and forth whether to have the child component utilize local state or have it handled by higher component and pass additional props down.
In this limited example, I have a Main component. I display some data in this component and pass functions to filter the data to a child component. Though, main component doesn't necessarily need to know about when the menuOpen property is changed. However, I need to update menuOpen when handleCancel(), handleSave(), and handleButtonClick() are called.
handleCancel() and handleSave() both modify the data that is displayed so I declare them in the Main component.
Should I be passing all these props through from Main component or use intermediate components to handle smaller portions of local state but also call props from a parent (grandparent etc) component?
Main Component
//Parent component
class Main extends React.Component {
constructor() {
super();
this.state = {
checkBoxes: {
1: {
name: 'Apple',
isChecked: true,
},
//...
},
fruit: {
1: {
name: 'Apple',
},
//...
},
checkedBoxes: [],
};
this.baseState = JSON.stringify(this.state.checkBoxes);
this.fruitFilter = this.fruitFilter.bind(this);
this.handleSave = this.handleSave.bind(this);
this.handleChange = this.handleChange.bind(this);
this.resetState = this.resetState.bind(this);
}
resetState() {
this.setState({checkBoxes: JSON.parse(this.baseState)});
}
//populates the checkedboxs array with name to filter by
handleSave() {
const checkedBoxes = Object.keys(this.state.checkBoxes)
.filter(key => {
//....some logic
});
this.baseState = JSON.stringify(this.state.checkBoxes);
this.setState({checkedBoxes: checkedBoxes});
}
//handles the checkbox toggle
handleChange(e) {
const checkBoxes = {...this.state.checkBoxes};
checkBoxes[e.target.id].isChecked = e.target.checked;
this.setState({checkBoxes: checkBoxes});
}
//filteres the fruit - if nothing is checked return them all
fruitFilter(fruit) {
return Object.keys(fruit)
.filter(key => {
//...filter logic
})
}
render() {
const visibleFruits = this.fruitFilter(this.state.fruit);
return (
<div>
<Filter
resetState={this.resetState}
checkBoxes={this.state.checkBoxes}
handleSave={this.handleSave}
handleChange={this.handleChange}
/>
<div>
<h2>Filtered Fruit</h2>
{Object.keys(visibleFruits).map(key => {
return (
//... renders list of fruit
);
})}
</div>
</div>
);
}
}
Child Component
class Filter extends React.Component {
constructor(props) {
super(props);
this.state = {
menoOpen: false,
};
this.handleCancel = this.handleCancel.bind(this);
this.handleSave = this.handleSave.bind(this);
this.handleButtonClick = this.handleButtonClick.bind(this);
}
handleSave() {
this.setState({menuOpen: false});
this.props.handleSave();
}
handleCancel() {
this.setState({menuOpen: false});
this.props.resetState();
}
handleButtonClick() {
this.setState({menuOpen: !this.state.menuOpen});
}
render() {
return (
<div>
<button onClick={this.handleButtonClick}>Choose Fruits</button>
{this.state.menuOpen && (
<FilterMenu
checkBoxes={this.props.checkBoxes}
handleSave={this.handleSave}
handleCancel={this.handleCancel}
handleChange={this.props.handleChange}
/>
)}
</div>
);
}
}
Grandchild Component
const FilterMenu = ({checkBoxes, handleChange, handleCancel, handleSave}) => {
return (
<div>
{Object.keys(checkBoxes).map(key => {
return (
//... renders dropdown menu
);
})}
<button onClick={handleCancel}>Cancel</button>
<button onClick={handleSave}>Save</button>
</div>
);
};
Refine the separation of concerns and I think you'll like it better.
Define all checkbox event handlers in Filter.
Filter communications with Main via state only.
Don't force Main to evaluate UI components to set state.
Define Main state for Filter to use as needed to avoid the above.
Filter will construct the checkboxes.
Cancel and Save buttons seem like Filter level functions to me.
A FilterMenu component now seems pointless because it does not do anything. Perhaps in the larger architecture it is useful but you can always re-factor it out of Filter when needed
Filter component is the seam in the code that separates action from state.
State is not unnecessarily pushed further down.
Actual functionality is not unnecessarily pushed further up.
Coupling between Main and Filter is reduced. Filter has more reuse potential.

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.

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

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

Appending React component without ANY redux?

I'm using ReactDOM.render to render into an element, but it completely overwrites existing nodes within it. I don't want to tie any redux-like state functionality, I just want to render to a string and append to a element. Is this possible?
I suspect people will tell me to just use states and what-not, but I guess my follow-up question to that would be, I have a element that will have hundreds/thousands of images, will it re-render everytime using React? Will manipulating a state with thousands of objects be slow?
Consider this example of products being listed:
https://jsfiddle.net/pf7z8z4m/12/
class Product extends React.Component{
shouldComponentUpdate(nextProps, nextState) {
return (this.props.product !== nextProps.product)
}
render(){
console.log('rendering product '+this.props.product.name)
return (<li>
<h4>{this.props.product.name}</h4>
<img src={this.props.product.img} alt={this.props.product.name}/>
</li>)
}
}
class ProductList extends React.Component{
render(){
const items = this.props.products.map((product)=>
<ul key={product.id}>
<Product product={product}/>
</ul>)
return (<ul>{items}</ul>)
}
}
class App extends React.Component{
constructor(props){
super(props)
this.state = {products:[]}
//following is just a simple simulation of products being added to the list through time
setTimeout(
()=>{this.setState({products:[...this.state.products,
{id:1,name:'Product A', img:'productA.png'}]})},
1000
)
setTimeout(
()=>{this.setState({products:[...this.state.products,
{id:2,name:'Product B', img:'productB.png'}]})},
3000
)
setTimeout(
()=>{this.setState({products:[...this.state.products,
{id:3,name:'Product C', img:'productC.png'}]})},
5000
)
}
render(){
return (<ProductList products={this.state.products}/>)
}
}
React.render(<App/>,document.getElementById('root'))
New elements (products) are being added to the list regularly, but not causing re-render on the rest.
The method shouldComponentUpdate on Product is controlling that. Another option is to extend React.PureComponent.
Take a look at this doc:
https://facebook.github.io/react/docs/optimizing-performance.html

Categories

Resources