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'}}}/>
Related
I'm trying to fetch videos from youtube and display on browser.. I'm able to get videos and update state property. ( checked with console log)
But SearchResults component is not re-rendering when I update the state property. Here is my component
class Application extends React.Component {
state = {videos: []};
handleFormSubmit = async term => {
const res = await youtubeApi.get("search", {
params: {q: term}
});
this.setState({videos: res.data.items})
};
render() {
return (
<div className="container">
<SearchForm onFormSubmit={this.handleFormSubmit}/>
<SearchResults videos={this.state.videos}/>
</div>
)
}
}
SearchResults component
class SearchResults extends React.Component {
state = {featured: null, suggested: []};
componentDidMount() {
if (this.props.videos.length > 0)
this.setState({featured: this.props.videos[0].id.videoId});
if (this.props.videos.length > 1)
this.setState({suggested: this.props.videos.slice(1)});
}
handleSidebarVideoClick = id => {
this.setState({featured: id})
};
render() {
return (
<div className="row">
<div className="col-md-8">
<Video video={this.state.featured}/>
</div>
<div className="col-md-4">
<Sidebar videos={this.state.suggested} onSidebarVideoClick={this.handleSidebarVideoClick}/>
</div>
</div>
)
}
}
You need to have getDerivedStateFromProps() in your SearchResults component in order to expect a new props value when it changes on parent component.
ComponentDidMount is called once the component is mounted, not when it receives prop values when it changes every time.
So you should have something like this:
static getDerivedStateFromProps(nextProps) {
if (nextProps.videos.length > 0) {
return {
featured: nextProps.videos[0].id.videoId
};
}
if (nextProps.videos.length > 1) {
return {
suggested: nextProps.videos.slice(1)
};
}
return null;
}
Ref: https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops
It is unlikely you checked the component's state property. You most likely checked this.state which does get updated. The question is what is this.
There is no evidence you bound your event handlers to the component as recommended in the dev guide. So the keyword this doesn't reflect the Component, it reflects the DOM element receiving the event.
To test this, console log the value of this in you event handlers.
To fix:
this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.handleSidebarVideoClick = this.handleSidebarVideoClick.bind(this);
in their respective classes constructors.
Ref: https://reactjs.org/docs/handling-events.html
First of all, I'm really new into React, so forgive my lack of knowledge about the subject.
As far as I know, when you setState a new value, it renders again the view (or parts of it that needs re-render).
I've got something like this, and I would like to know if it's a good practice or not, how could I solve this kind of issues to improve, etc.
class MyComponent extends Component {
constructor(props) {
super(props)
this.state = {
key: value
}
this.functionRender = this.functionRender.bind(this)
this.changeValue = this.changeValue.bind(this)
}
functionRender = () => {
if(someParams !== null) {
return <AnotherComponent param={this.state.key} />
}
else {
return "<span>Loading</span>"
}
}
changeValue = (newValue) => {
this.setState({
key: newValue
})
}
render() {
return (<div>... {this.functionRender()} ... <span onClick={() => this.changeValue(otherValue)}>Click me</span></div>)
}
}
Another component
class AnotherComponent extends Component {
constructor (props) {
super(props)
}
render () {
return (
if (this.props.param === someOptions) {
return <div>Options 1</div>
} else {
return <div>Options 2</div>
}
)
}
}
The intention of the code is that when I click on the span it will change the key of the state, and then the component <AnotherComponent /> should change because of its parameter.
I assured that when I make the setState, on the callback I throw a console log with the new value, and it's setted correctly, but the AnotherComponent doesn't updates, because depending on the param given it shows one thing or another.
Maybe I need to use some lifecycle of the MyComponent?
Edit
I found that the param that AnotherComponent is receiving it does not changes, it's always the same one.
I would suggest that you'll first test it in the parent using a simple console.log on your changeValue function:
changeValue = (newValue) => {
console.log('newValue before', newValue);
this.setState({
key: newValue
}, ()=> console.log('newValue after', this.state.key))
}
setState can accept a callback that will be invoked after the state actually changed (remember that setState is async).
Since we can't see the entire component it's hard to understand what actually goes on there.
I suspect that the newValue parameter is always the same but i can't be sure.
It seems like you're missing the props in AnotherComponent's constructor. it should be:
constructor (props) {
super(props) // here
}
Try replacing the if statement with:
{this.props.param === someOptions? <div>Options 1</div>: <div>Options 2</div>}
also add this function to see if the new props actually get to the component:
componentWillReceiveProps(newProps){
console.log(newProps);
}
and check for the type of param and someOptions since you're (rightfully) using the === comparison.
First, fat arrow ( => ) autobind methods so you do not need to bind it in the constructor, second re-renders occur if you change the key of the component.
Ref: https://reactjs.org/docs/lists-and-keys.html#keys
I have a react component, which has properties and state. Some fields of state contain input data (uplifted from input control), but there is also fields in the state that must be Calculated based on current State and Props:
The question: what is the best way to update calculated fields of the state (based on other fields of state and props)?
Ugly way to do it:
componentDidUpdate(){
this.setState({calculatedField:calculate(this.props,this.state)}))
}
In this case I get infinite loop of updates or in the best case (if I use PureComponent) double rendering invocation.
The best solution I found so far (but still ugly):
Is to create a calculated object in state, which contains calculated fields and updated in componentWillUpdate avoiding setState:
componentWillUpdate(nextProps,nextState){
nextState.calculated.field1=f(nextProps,nextState)
}
class ParentComponent extends React.Component {
constructor(props, ctx) {
super(props,ctx)
this.state={A:"2"}
}
render() {
console.log("rendering ParentComponent")
return <div>
<label>A=<input value={this.state.A} onChange={e=>{this.setState({A:e.target.value})}} /></label> (stored in state of Parent component)
<ChildComponent A={this.state.A} />
</div>
}
}
class ChildComponent extends React.PureComponent {
constructor(props,ctx) {
super(props,ctx);
this.state={
B:"3",
Calculated:{}
}
}
render() {
console.log("rendering ChildComponent")
return <div>
<label>B=<input value={this.state.B} onChange={e=>{this.setState({B:e.target.value})}} /></label> (stored in state of Child component state)
<div>
f(A,B)=<b>{this.state.Calculated.result||""}</b>(stored in state of Child component)
<button onClick={e=>{ this.setState({Calculated:{result:new Date().toTimeString()}}) }}>Set manual value</button>
</div>
</div>
}
componentWillUpdate(nextProps, nextState) {
this.state.Calculated.result = getCalculatedResult(nextProps.A, nextState.B)
}
componentWillReceiveProps(nextProps) {
this.state.Calculated.result = getCalculatedResult(nextProps.A, this.state.B)
}
componentWillMount() {
this.state.Calculated.result = getCalculatedResult(this.props.A, this.state.B)
}
}
function getCalculatedResult(a,b) {
const aNum = Number(a)||0
const bNum = Number(b)||0;
const result = (aNum*bNum).toString();
return result;
}
ReactDOM.render(<ParentComponent/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.development.js"></script>
<div id="root"></div>
This is also ugly solution and React does not recommended to mutate state avoiding setState. So what is right solution for that?
NOTE:
In my real application I cannot recalculate f(a,b) every single time during rendering, because it's actually complex object, so I need to cache it somehow and the best way is in the state.
If you are using React 16.8.0 and above, you can use React hooks API. I think it's useMemo() hook you might need. For example:
import React, { useMemo } from 'react'
const MyComponent = ({ ...props }) => {
const calculatedValue = useMemo(
() => {
// Do expensive calculation and return.
},
[a, b]
)
return (
<div>
{ calculatedValue }
</div>
)
}
For more details, refer to the React documentation
I wouldn't advice you to store your calculated value inside your state. My approach would be more like this:
import PropTypes from 'prop-types';
import React from 'react';
class Component extends React.Component {
static defaultProps = { value: 0 };
static propTypes = { value: PropTypes.number };
state = { a: 0, b: 0 };
result = () => this.state.a + this.state.b + this.props.value;
updateA = e => this.setState({ a: +e.target.value });
updateB = e => this.setState({ b: +e.target.value });
render() {
return (
<div>
A: <input onChange={this.updateA} value={this.state.a} />
B: <input onChange={this.updateB} value={this.state.b} />
Result: {this.result()}
</div>
);
}
}
The problem with storing the calculation inside your state is, that the calculation can be mutated by multiple sources. If you use my solution, there is no way, that anything can overwrite the calculation WITHOUT using the correct function to calculate them.
You can save calculated result in this.calculated instead of this.state. It is dependent data. All data which causes update and render is already in state and props.
class Component extends React.Component {
constructor(props) {
super(props)
state = {
b: 0
}
}
updateThis = (event) => {
this.setState({ b: event.target.value });
}
componentWillUpdate(nextProps,nextState){
this.calculated.field1=f(nextProps.a, nextState.b)
}
render() {
return (
<form>
A = <input onChange={this.props.updateParent} value={this.props.a} /> <br>
B = <input onChange={this.updateThis} value={this.state.b} /> <br>
f(A,B) = {this.calculated.field1} <br>
</form>
);
}
}
class ParentComponent extends React.Component {
constructor(props) {
super(props)
state = {
a: 0
}
}
render() {
return (
<Component
updateParent={event=>this.setState({a: event.target.value})}
a={this.state.a}
/>
}
}
}
You're first attempt is the right way to solve this problem. However, you need to add a check to see if state has actually changed:
componentDidUpdate(prevProps, prevState){
if(prevState.field !== this.state.field){
this.setState({calculatedField:calculate(this.props,this.state)}))
}
}
shouldComponentUpdate(nextProps, nextState) {
return this.state.calculatedField !== nextState.calculatedField
}
You need to check the pieces of state and props that you use in your calculate method and make sure they have changed before updating state again. This will prevent the infinite loop.
It looks like the "state" is the place to store everything (even computed values) you'll need to use on the render function, but usually we have the problem you describe.
Since React 16.3 a new approach for this situation has been given in the way of the static getDerivedStateFromProps (nextProps, prevState) "lifecycle hook".
You should update at least to this version if you haven't, and follow the advice given by the React Team on their blog.
Here is the official documentation for this functionality.
The issue here is that this function is invoked before every render, and being "static" you cannot access the current instance previous props, which is usually needed to decide if the computed value must be generated again or not (I suppose this is your case, as you have stated your computation process is heavy). In this case, the React team suggests to store in the state the values of the related props, so they can be compared with the new ones:
if (nextProps.propToCompute !== prevState.propToComputePrevValue) {
return {
computedValue: Compute(nextProp.propToCompute),
propToComputePrevValue: nextProps.propToCompute
};
}
return null;
Do not include redundant information in your state.
A simplified example is having firstName and lastName in your state. If we want to display the full name in your render method, you would simply do:
render() {
return <span>{`${this.state.firstName} ${this.state.lastName}`}</span>
}
I like this example because it's easy to see that adding a fullName in our state, that just holds ${this.state.firstName} ${this.state.lastName} is unnecessary. We do string concatenation every time our component renders, and we're okay with that because it's a cheap operation.
In your example, your calculation is cheap so you should do it in the render method as well.
This question already has answers here:
Reactjs - Setting State from props using setState in child component
(2 answers)
Closed 5 years ago.
So as I understand, a component will re-render when there has been a change in props and componentWillMount shall run before re-rendering. At the moment my constructor and componentWillMount run as expected, but then the question prop changes which I need to update the user score state, but this change in question prop doesn't trigger the constructor or componentWillMount. As I shouldn't update the state inside the render function (the only place so far that I have been able to get access to the updated question prop), how can I make react recognise this change in it's props and then update the state? Hope that's understandable.
Here is my container
class FullTimeScoreContainer extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
userHomeScore: 1,
userAwayScore: 1
}
}
componentWillMount() {
getFTSAnswerStatus(this.props.question).then(foundScores => {
if ( foundScores.userHomeScore ) {
this.setState({
userHomeScore: foundScores.userHomeScore,
userAwayScore: foundScores.userAwayScore
});
}
})
}
render() {
const { option, question, questionIndex, user, configs, renderAs, showNextQuestionAfterFTS, total} = this.props;
// check if question is active or not
let ComponentClass;
if ( question[0].active ) {
ComponentClass = FullTimeScoreActive;
} else {
ComponentClass = FullTimeScoreLocked;
}
const changeScoreState = (team, val) => {
switch (team) {
case "home":
this.setState( (prevState) => ({ userHomeScore: prevState.userHomeScore + val }) );
break;
case "away":
this.setState( (prevState) => ({ userAwayScore: prevState.userAwayScore + val }) );
break;
default:
throw new Error("unrecognised team to change score state")
}
}
const onClickCallback = () => {
const p = this.props;
const s = this.state;
p.showNextQuestionAfterFTS();
p.recordFullTimeScoreAnswer(s.userHomeScore, s.userAwayScore, p.question, p.questionIndex, p.user, p.configs)
}
return (
<ComponentClass
imgSrc={imgSrc}
user={user}
answerStatus={answerStatus}
question={question}
onClickCallback={onClickCallback}
questionIndex={questionIndex}
total={total}
configs={configs}
userScores={this.state}
changeScoreState={changeScoreState}
/>
)
}
}
const mapStateToProps = state => {
return {
configs: state.configs,
user: state.user
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({ recordFullTimeScoreAnswer, showNextQuestionAfterFTS }, dispatch);
};
export default connect(mapStateToProps, mapDispatchToProps)(FullTimeScoreContainer);
export { FullTimeScoreContainer }
componentWillMount will only run before the first render. It doesn't get run before every render. So even if your state and props update, componentWillMount will not get called again.
The constructor function is the same as well.
You might be looking for componentWillReceiveProps (see docs). This lifecycle event is called when a mounted component is about to receive new props. You can update your state in this lifecycle event.
Note that componentWillReceiveProps only works on mounted components. Therefore, it will not get called the first time your component receives its' initial props.
A side note: Per the docs, you also don't want to introduce any side-effects or subscriptions in componentWillMount. Do that in componentDidMount instead.
I would like add a comment, but I don't have enough reputation...
a component will re-render when there has been a change in props
As I understand, you can't change the props, so component re-render on state changes.
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