How to re-render child on parent event in ReactJS? [duplicate] - javascript

The Parent (MyList in my example) component renders an array thru a Child (MyComponent) component. Parent decides to change properties in the array, what is React way of triggering child re-rendering?
All I came up with is this.setState({}); in Parent after tweaking the data. Is this a hack or a React way of triggering an update?
JS Fiddle:
https://jsfiddle.net/69z2wepo/7601/
var items = [
{id: 1, highlighted: false, text: "item1"},
{id: 2, highlighted: true, text: "item2"},
{id: 3, highlighted: false, text: "item3"},
];
var MyComponent = React.createClass({
render: function() {
return <div className={this.props.highlighted ? 'light-it-up' : ''}>{this.props.text}</div>;
}
});
var MyList = React.createClass({
toggleHighlight: function() {
this.props.items.forEach(function(v){
v.highlighted = !v.highlighted;
});
// Children must re-render
// IS THIS CORRECT?
this.setState({});
},
render: function() {
return <div>
<button onClick={this.toggleHighlight}>Toggle highlight</button>
{this.props.items.map(function(item) {
return <MyComponent key={item.id} text={item.text} highlighted={item.highlighted}/>;
})}
</div>;
}
});
React.render(<MyList items={items}/>, document.getElementById('container'));

The problem here is that you're storing state in this.props instead of this.state. Since this component is mutating items, items is state and should be stored in this.state. (Here's a good article on props vs. state.) This solves your rendering problem, because when you update items you'll call setState, which will automatically trigger a re-render.
Here's what your component would look like using state instead of props:
var MyList = React.createClass({
getInitialState: function() {
return { items: this.props.initialItems };
},
toggleHighlight: function() {
var newItems = this.state.items.map(function (item) {
item.highlighted = !item.highlighted;
return item;
});
this.setState({ items: newItems });
},
render: function() {
return (
<div>
<button onClick={this.toggleHighlight}>Toggle highlight</button>
{ this.state.items.map(function(item) {
return <MyComponent key={item.id} text={item.text}
highlighted={item.highlighted}/>;
}) }
</div>
);
}
});
React.render( <MyList initialItems={initialItems}/>,
document.getElementById('container') );
Note that I renamed the items prop to initialItems, because it makes it clear that MyList will mutate it. This is recommended by the documentation.
You can see the updated fiddle here: https://jsfiddle.net/kxrf5329/

I have found a nice solution using key attribute for re-render with React Hook. If we changed key property of a child component or some portion of React Component, it will re-render entirely. It will use when you need to re-render some portion of React Component of re-render a child component. Here is a example. I will re-render the full component.
import React, { useState, useEffect } from "react";
import { PrEditInput } from "./shared";
const BucketInput = ({ bucketPrice = [], handleBucketsUpdate, mood }) => {
const data = Array.isArray(bucketPrice) ? bucketPrice : [];
const [state, setState] = useState(Date.now());
useEffect(() => {
setState(Date.now());
}, [mood, bucketPrice]);
return (
<span key={state}>
{data.map((item) => (
<PrEditInput
key={item.id}
label={item?.bucket?.name}
name={item.bucketId}
defaultValue={item.price}
onChange={handleBucketsUpdate}
mood={mood}
/>
))}
</span>
);
};
export default BucketInput;

An easy option to re-render a child is to update a unique key attribute every time you need a re-render.
<ChildComponent key={this.state.updatedKey}/>

You should trigger a re-rendering by calling setState() and giving the new props you want to propagate down.
If you really want to force an update you can also call forceUpdate().
If you look at the examples on this page, you can see that setState is the method used to update and trigger a re-rendering. The documentation is also stating (ahaha!) that clearly.
In your case I would call forceUpdate.
EDIT: As Jordan mentioned in the comment, it would be better to store items as part of your state. That way you wouldn't have to call forceUpdate but you would really update the state of your component, thus a regular setState with the updated values would work better.

You can set a numeric key on the child component and trigger a key change once an action is performed. e.g
state = {
childKey: 7,
};
<ChildComponent key={this.state.childKey}/>
actionToTriggerReload = () => {
const newKey = this.state.childKey * 89; // this will make sure the key are never the same
this.setState({childKey: newKey})
}
This will surely re-render the ChildComponent

Set a numeric default 'key' in the child component and to re-render just change key value.
this.state = {
updatedKey: 1,
};
triggerReload = () => {
let newKey = Math.floor(Math.random() * 100); // make sure the key are never the same
this.setState({updatedKey: newKey})
}
<childComponent key={this.state.updatedKey} handlerProp = {this.onClickItemEvent} />
This worked for me to re-render the ChildComponent in reactjs class base

Related

Passing data up through nested Components in React

Prefacing this with a thought; I think I might require a recursive component but that's beyond my current ability with native js and React so I feel like I have Swiss cheese understanding of React at this point.
The problem:
I have an array of metafields containing metafield objects with the following structure:
{
metafields: [
{ 0:
{ namespace: "namespaceVal",
key: "keyVal",
val: [
0: "val1",
1: "val2",
2: "val3"
]
}
},
...
]
}
My code maps metafields into Cards and within each card lives a component <MetafieldInput metafields={metafields['value']} /> and within that component the value array gets mapped to input fields. Overall it looks like:
// App
render() {
const metafields = this.state.metafields;
return (
{metafields.map(metafield) => (
<MetafieldInputs metafields={metafield['value']} />
)}
)
}
//MetafieldInputs
this.state = { metafields: this.props.metafields}
render() {
const metafields = this.state;
return (
{metafields.map((meta, i) => (
<TextField
value={meta}
changeKey={meta}
onChange={(val) => {
this.setState(prevState => {
return { metafields: prevState.metafields.map((field, j) => {
if(j === i) { field = val; }
return field;
})};
});
}}
/>
))}
)
}
Up to this point everything displays correctly and I can change the inputs! However the change happens one at a time, as in I hit a key then I have to click back into the input to add another character. It seems like everything gets re-rendered which is why I have to click back into the input to make another change.
Am I able to use components in this way? It feels like I'm working my way into nesting components but everything I've read says not to nest components. Am I overcomplicating this issue? The only solution I have is to rip out the React portion and take it to pure javascript.
guidance would be much appreciated!
My suggestion is that to out source the onChange handler, and the code can be understood a little bit more easier.
Mainly React does not update state right after setState() is called, it does a batch job. Therefore it can happen that several setState calls are accessing one reference point. If you directly mutate the state, it can cause chaos as other state can use the updated state while doing the batch job.
Also, if you out source onChange handler in the App level, you can change MetafieldInputs into a functional component rather than a class-bases component. Functional based component costs less than class based component and can boost the performance.
Below are updated code, tested. I assume you use Material UI's TextField, but onChangeHandler should also work in your own component.
// Full App.js
import React, { Component } from 'react';
import MetafieldInputs from './MetafieldInputs';
class App extends Component {
state = {
metafields: [
{
metafield:
{
namespace: "namespaceVal",
key: "keyVal",
val: [
{ '0': "val1" },
{ '1': "val2" },
{ '2': "val3" }
]
}
},
]
}
// will never be triggered as from React point of view, the state never changes
componentDidUpdate() {
console.log('componentDidUpdate')
}
render() {
const metafields = this.state.metafields;
const metafieldsKeys = Object.keys(metafields);
const renderInputs = metafieldsKeys.map(key => {
const metafield = metafields[key];
return <MetafieldInputs metafields={metafield.metafield.val} key={metafield.metafield.key} />;
})
return (
<div>
{renderInputs}
</div>
)
}
}
export default App;
// full MetafieldInputs
import React, { Component } from 'react'
import TextField from '#material-ui/core/TextField';
class MetafieldInputs extends Component {
state = {
metafields: this.props.metafields
}
onChangeHandler = (e, index) => {
const value = e.target.value;
this.setState(prevState => {
const updateMetafields = [...prevState.metafields];
const updatedFields = { ...updateMetafields[index] }
updatedFields[index] = value
updateMetafields[index] = updatedFields;
return { metafields: updateMetafields }
})
}
render() {
const { metafields } = this.state;
// will always remain the same
console.log('this.props', this.props)
return (
<div>
{metafields.map((meta, i) => {
return (
<TextField
value={meta[i]}
changekey={meta}
onChange={(e) => this.onChangeHandler(e, i)}
// generally it is not a good idea to use index as a key.
key={i}
/>
)
}
)}
</div>
)
}
}
export default MetafieldInputs
Again, IF you out source the onChangeHandler to App class, MetafieldInputs can be a pure functional component, and all the state management can be done in the App class.
On the other hand, if you want to keep a pure and clean App class, you can also store metafields into MetafieldInputs class in case you might need some other logic in your application.
For instance, your application renders more components than the example does, and MetafieldInputs should not be rendered until something happened. If you fetch data from server end, it is better to fetch the data when it is needed rather than fetching all the data in the App component.
You need to do the onChange at the app level. You should just pass the onChange function into MetaFieldsInput and always use this.props.metafields when rendering

Use dynamically created react components and fill with state values

Below is a proof of concept pen. I'm trying to show a lot of input fields and try to collect their inputs when they change in one big object. As you can see, the input's won't change their value, which is what I expect, since they're created once with the useEffect() and filled that in that instance.
I think that the only way to solve this is to use React.cloneElement when values change and inject the new value into a cloned element. This is why I created 2000 elements in this pen, it would be a major performance hog because every element is rerendered when the state changes. I tried to use React.memo to only make the inputs with the changed value rerender, but I think cloneElement simply rerenders it anyways, which sounds like it should since it's cloned.
How can I achieve a performant update for a single field in this setup?
https://codepen.io/10uur/pen/LYPrZdg
Edit: a working pen with the cloneElement solution that I mentioned before, the noticeable performance problems and that all inputs rerender.
https://codepen.io/10uur/pen/OJLEJqM
Here is one way to achieve the desired behavior :
https://codesandbox.io/s/elastic-glade-73ivx
Some tips :
I would not recommend putting React elements in the state, prefer putting plain data (array, objects, ...) in the state that will be mapped to React elements in the return/render method.
Don't forget to use a key prop when rendering an array of elements
Use React.memo to avoid re-rendering components when the props are the same
Use React.useCallback to memoize callback (this will help when using React.memo on children)
Use the functional form of the state setter to access the old state and update it (this also helps when using React.useCallback and avoid recreating the callback when the state change)
Here is the complete code :
import React, { useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const INPUTS_COUNT = 2000;
const getInitialState = () => {
const state = [];
for (var i = 0; i < INPUTS_COUNT; i++) {
// Only put plain data in the state
state.push({
value: Math.random(),
id: "valueContainer" + i
});
}
return state;
};
const Root = () => {
const [state, setState] = React.useState([]);
useEffect(() => {
setState(getInitialState());
}, []);
// Use React.useCallback to memoize the onChangeValue callback, notice the empty array as second parameter
const onChangeValue = React.useCallback((id, value) => {
// Use the functional form of the state setter, to update the old state
// if we don't use the functional form, we will be forced to put [state] in the second parameter of React.useCallback
// in that case React.useCallback will not be very useful, because it will recreate the callback whenever the state changes
setState(state => {
return state.map(item => {
if (item.id === id) {
return { ...item, value };
}
return item;
});
});
}, []);
return (
<>
{state.map(({ id, value }) => {
// Use a key for performance boost
return (
<ValueContainer
id={id}
key={id}
onChangeValue={onChangeValue}
value={value}
/>
);
})}
</>
);
};
// Use React.memo to avoid re-rendering the component when the props are the same
const ValueContainer = React.memo(({ id, onChangeValue, value }) => {
const onChange = e => {
onChangeValue(id, e.target.value);
};
return (
<>
<br />
Rerendered: {Math.random()}
<br />
<input type="text" value={value} onChange={onChange} />
<br />
</>
);
});
ReactDOM.render(<Root />, document.getElementById("root"));

Force a child component to update when the parent state changes

I have a MyList component that fetches items, allows filtering and sorting. This component is already used in other parts of the apps and it works well. It uses render props to render the items so it accepts a renderItem prop of type function.
Now I'm building a simple list to allow item selection using the aforementioned component and I'm checking for the selected state in the render prop renderItem method. The problem is that when I change the state of MySelectableList the MyList component doesn't update because its props does not change (it's always the same bound function renderProp). For now I forced the rendering of the child with this.renderItem = this.renderItem.bind(this); but I don't like it, I know that I can update the child component with ref but I don't like it either.
Is there a better method to force the child component to render when the parent state changes? Am I doing something wrong?
Full code of MySelectableList:
class MySelectableList extend Component {
constructor (props) {
super(props);
this.state = {
selectedItems: [],
};
this.renderItem = this.renderItem.bind(this);
this.toggle = this.toggle.bind(this);
this.isSelected = this.isSelected.bind(this);
}
toggle (item) {
const newItems = this.state.selectedItems.slice(0);
const index = newItems.indexOf(item.uuid);
if (index === -1) {
newItems.push(item.uuid);
} else {
newItems.splice(index, 1);
}
this.setState({ selectedItems: newItems });
// Force MyList to re-render by tricking react that it's different
this.renderItem = this.renderItem.bind(this);
}
isSelected (item) {
return this.state.selectedItems.includes(item.uuid);
}
renderItem (item) {
return (<MySelectableItem
key={ item.uuid }
item={ item }
toggle={ this.toggle }
selected={ this.isSelected(item) } />);
}
render () {
return (
<div>
...
<MyList renderItem={ this.renderItem } />
...
</div>
);
}
}
Thanks in advance.
EDIT
The MyList component is connected to redux store using connect. I discovered that connect is the cause of the MyList component missing rendering, using only "vanilla" react component it works correctly.
I reproduced the problem in this codesandbox: https://codesandbox.io/s/0mov14nmmp
Since you asked about how to do it more react friendly way
The nicer way to do this would be:
render () {
return (
<div>
<MyList {...whateeverExtraPropsyouWantToPass}>
<MySelectableItem
key={ item.uuid }
item={ item }
toggle={ this.toggle }
selected={ this.isSelected(item) } />
</MyList>
</div>
);
Then your MyList will look something like this:
render () {
return (
<div>
...//your other MyList code
...
{this.props.children}
</div>
);
This looks more readable, more maintainable and easily debuggble. But I'm sure this is all obvious to you. Since, you asked about a react friendly way, this is the most react friendly way you can do.
I would not suggest unnecessary, explicitly trying to render any component. Until and unless it is the only way, which is not the case in your component.
There is nothing wrong with the way you implement MyList. React Native FlatList has the same pattern. But why dont you also pass items as a property to the MyList, so it will be like
<MyList items={this.state. selectedItems} renderItem={this.renderItem} />
This way MyList will re-render because items property changes. items property is needed as well because I assume that in your MyList component you need to do items.map function right? otherwise how do you know how many items in total you need to render?

React.js: Re-render component data on add

New object is added fine and gets newTodos with updated data. In render function I get this.props.todoList which is an array of objects to be displayed.
How can I update todoList with newTodos to display this new data?
I can do this with setState({todoList: newTodos}) and in render function get this.state.todoList etc but I don't want to keep big objects (in future) in state. Instead I want to use props.
Any suggestion?
var React = require('react');
var TodoList = require('./todo-list.js');
var App = React.createClass({
getInitialState: function () {
return { filter: 'active' };
},
onAdd: function (txt, color) {
console.log('txt: ' + txt + ', color: ' + color);
var newTodos = this.props.todoList.push(Map({ txt: txt, isCompleted: false, color: color }));
this.forceUpdate();
// this.setState(this.state);
},
render: function () {
var { todoList } = this.props;
return (
<div>
<TodoList todoList={todoList}/>
</div>
);
}
});
module.exports = App;
Props only purpose in React is to transfer data from parent component to children in order to notify them that state has changed. You will have to maintain state because it's the right React way to control application components and maintain state integrity. Not sure why you prefer props over state as you'll have to store data somewhere anyway, but the provided solution with forceUpdate() will soon make your app state inconsistent as long as you'll add more and more components.

Trigger child re-rendering in React.js

The Parent (MyList in my example) component renders an array thru a Child (MyComponent) component. Parent decides to change properties in the array, what is React way of triggering child re-rendering?
All I came up with is this.setState({}); in Parent after tweaking the data. Is this a hack or a React way of triggering an update?
JS Fiddle:
https://jsfiddle.net/69z2wepo/7601/
var items = [
{id: 1, highlighted: false, text: "item1"},
{id: 2, highlighted: true, text: "item2"},
{id: 3, highlighted: false, text: "item3"},
];
var MyComponent = React.createClass({
render: function() {
return <div className={this.props.highlighted ? 'light-it-up' : ''}>{this.props.text}</div>;
}
});
var MyList = React.createClass({
toggleHighlight: function() {
this.props.items.forEach(function(v){
v.highlighted = !v.highlighted;
});
// Children must re-render
// IS THIS CORRECT?
this.setState({});
},
render: function() {
return <div>
<button onClick={this.toggleHighlight}>Toggle highlight</button>
{this.props.items.map(function(item) {
return <MyComponent key={item.id} text={item.text} highlighted={item.highlighted}/>;
})}
</div>;
}
});
React.render(<MyList items={items}/>, document.getElementById('container'));
The problem here is that you're storing state in this.props instead of this.state. Since this component is mutating items, items is state and should be stored in this.state. (Here's a good article on props vs. state.) This solves your rendering problem, because when you update items you'll call setState, which will automatically trigger a re-render.
Here's what your component would look like using state instead of props:
var MyList = React.createClass({
getInitialState: function() {
return { items: this.props.initialItems };
},
toggleHighlight: function() {
var newItems = this.state.items.map(function (item) {
item.highlighted = !item.highlighted;
return item;
});
this.setState({ items: newItems });
},
render: function() {
return (
<div>
<button onClick={this.toggleHighlight}>Toggle highlight</button>
{ this.state.items.map(function(item) {
return <MyComponent key={item.id} text={item.text}
highlighted={item.highlighted}/>;
}) }
</div>
);
}
});
React.render( <MyList initialItems={initialItems}/>,
document.getElementById('container') );
Note that I renamed the items prop to initialItems, because it makes it clear that MyList will mutate it. This is recommended by the documentation.
You can see the updated fiddle here: https://jsfiddle.net/kxrf5329/
I have found a nice solution using key attribute for re-render with React Hook. If we changed key property of a child component or some portion of React Component, it will re-render entirely. It will use when you need to re-render some portion of React Component of re-render a child component. Here is a example. I will re-render the full component.
import React, { useState, useEffect } from "react";
import { PrEditInput } from "./shared";
const BucketInput = ({ bucketPrice = [], handleBucketsUpdate, mood }) => {
const data = Array.isArray(bucketPrice) ? bucketPrice : [];
const [state, setState] = useState(Date.now());
useEffect(() => {
setState(Date.now());
}, [mood, bucketPrice]);
return (
<span key={state}>
{data.map((item) => (
<PrEditInput
key={item.id}
label={item?.bucket?.name}
name={item.bucketId}
defaultValue={item.price}
onChange={handleBucketsUpdate}
mood={mood}
/>
))}
</span>
);
};
export default BucketInput;
An easy option to re-render a child is to update a unique key attribute every time you need a re-render.
<ChildComponent key={this.state.updatedKey}/>
You should trigger a re-rendering by calling setState() and giving the new props you want to propagate down.
If you really want to force an update you can also call forceUpdate().
If you look at the examples on this page, you can see that setState is the method used to update and trigger a re-rendering. The documentation is also stating (ahaha!) that clearly.
In your case I would call forceUpdate.
EDIT: As Jordan mentioned in the comment, it would be better to store items as part of your state. That way you wouldn't have to call forceUpdate but you would really update the state of your component, thus a regular setState with the updated values would work better.
You can set a numeric key on the child component and trigger a key change once an action is performed. e.g
state = {
childKey: 7,
};
<ChildComponent key={this.state.childKey}/>
actionToTriggerReload = () => {
const newKey = this.state.childKey * 89; // this will make sure the key are never the same
this.setState({childKey: newKey})
}
This will surely re-render the ChildComponent
Set a numeric default 'key' in the child component and to re-render just change key value.
this.state = {
updatedKey: 1,
};
triggerReload = () => {
let newKey = Math.floor(Math.random() * 100); // make sure the key are never the same
this.setState({updatedKey: newKey})
}
<childComponent key={this.state.updatedKey} handlerProp = {this.onClickItemEvent} />
This worked for me to re-render the ChildComponent in reactjs class base

Categories

Resources