Having read a bunch of articles on the web about shouldComponentUpdate & render I want to double check if I get it correct when components are rerendered (render method is called) and when shouldComponentUpdate is called.
React docs (and dozens or articles say) that shouldComponentUpdate is called ONLY when new props are received or there is new state. But is seems that PureComponent does the same at first glance...
So to investigate it I wrote sample app:
parent component
import React, { Component } from 'react';
import './App.css';
import Hello from './Hello';
class App extends Component {
objWithName = { name: 'World' };
state = {
date: Date.now()
}
componentDidMount() {
setInterval(() => {
this.setState({ date: Date.now() });
}, 2000);
}
render() {
return (
<div className="App">
<p>Rendering timestamp {this.state.date}</p>
<Hello name={this.objWithName} />
</div>
);
}
}
export default App;
child component
import React from 'react';
export default class Hello extends React.Component {
shouldComponentUpdate() {
console.log('shouldComponentUpdate called');
return true;
}
render() {
console.log('render called');
return <p>Hello {this.props.name.name}</p>
}
}
So my first question is: is it completely true? I mean: In above code snippets, parent component in setInterval calls setState just to trigger its update, but this state is not used anywhere. And after doing that, child component is rerendered (render is called) & shouldComponentUpdate is called even though nothing has changed for him (it didn't receive any new props, nor state). I didn't find any explanation for this behaviour in React docs so I'm not sure how it works. What is more, if that child component didn't have any input props at (simply render static string) it would also get rerendered. Can sb explain it?
So my second question is: what does it mean new props/state is received by the component? Does it mean that object value is changed (for primitives simply new value, and for objects new reference)?
Third thing: assuming that a change in parent top most component (e.g. App.js) in the application happens (new prop or new state), does it mean that by default ALL react component that are currently rendered/mounted (even leafs that do not have any state, nor props that were changed) will rerender?
shouldComponentUpdate is just called from setState and forces render when returns true (default behaviour) - and updating DOM when different. This is by design to have a lifecycle/flow. When shouldComponentUpdate returns false then you can avoid unnecessary, usually costly render processing (and DOM update). PureComponent is just an optimized, ready to use component with shouldComponentUpdate defined to shallowly compare props (by comparing references).
React doesn't care if props/state are used, not using observables - you can use mobx for that.
New props means any change - it's shouldComponentUpdate responsibility to check swallowly or deep, depends what you need.
Yes, all children will be updated (forced to refresh node state/view in virtual DOM). It's fast/optimized process (operating on virtual tree) but they should use shouldComponentUpdate to limit theirs rerendering (and final DOM updates).
Related
I inherited some code to maintain, with this line in it:
this.setState({}); // Force update
Is this guaranteed to cause a re-render?
setState merges the object passed as argument into the actual state in an immutable way. this.setState({}) will merge nothing to state but will actually return a new object, the shallow comparison performed by React will always assert to false and a re render will be triggered, unless explicitly cancelled with shouldComponentUpdate. So yes, in this case it is equivalent to forceUpdate and it comes with the same caveats.
You can actually test it easily:
import React, { Component } from 'react';
import { Button } from 'react-native';
class Test extends Component {
render() {
console.log('render');
return <Button onPress={() => this.setState({})} title='Test' />;
}
}
export default Test;
Every time the button is clicked, the console.log triggers.
An interesting point is that if you replace this.setState({}) by this.setState(), there is no re-render after a click.
According to the documentation it is and several other methods in order
static getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()
please see this link for detailed
It depends. If you want to render a component, react internally checks is DOM equals with previous(this occurs if props of component is not changed at all). If dom equals with previous version, react checks shouldComponentUpdate. forceUpdate is different than this.setState({}), which always render components.
I am working on the issue regarding parent component render twice and child component also render twice. the existing code for the parent is
<childform
value ="name"
serverapi={this.valueservice.api}
update={this.update}
/>
the child component start render using
componentDidMount()
{
this.callservice(serverapi)
}
since its componentDidMount function called twice, the API also render twice which has to be avoided, each time the parent render the child's state is been refreshed so I cannot compare with the state, is it possible anyway to check or solution to this issue that I can refer? that resolves how many time I call the parent it call the API once
It seems that the update prop you are passing to childform is a boolean indicating whether or not the component should rerender. If that is the case you can use the shouldComponentUpdate() lifecycle method the to check if there is an update and only rerender when true:
shouldComponentUpdate(nextProps, nextState) {
//You can perform more logic here such as
//return this.props.update === nextProps.update
return nextProps.update;
}
You can find more information here:
https://reactjs.org/docs/react-component.html#shouldcomponentupdate
You can have a global variable called cache.
What this cache do is to check if your_awesome_api is called before.
This code below is a common approach:
// cache.js
let cache = {};
export default cache;
// someComponent.js
import cache from './cache';
// ...ignore some react component code...
componentDidMount() {
if (cache['your_awesome_api']) {
doSomething(cache['your_awesome_api'])
} else {
this.callservice(your_awesome_api).then(result => {
cache['your_awesome_api'] = result
})
}
}
This way, you don't need to worry about re-rendering problem ever!
Re-rendering in react is really common!
I have an npm imported component (CKEditor) that only cares about the state of its parent component at the time it mounts. I.e., no matter what changes occur in the state of the parent component, CKEditor won't reflect those changes if it has already mounted.
This is an issue for me because I need CKEditor to change based on the state of the parent component when the parent component changes its language prop.
Is there a way for me to make a child component unmount and mount again from the parent component? For example, is there a way for me to unmount and mount again a child component based on the parent component's "componentWillReceiveProps"?
import React from 'react';
import CKEditor from "react-ckeditor-component";
export default class EditParagraph extends React.Component {
constructor(props) {
super(props)
this.state = {
// an object that has a different html string for each potential language
content: this.props.specs.content,
}
this.handleRTEChange = this.handleRTEChange.bind(this)
this.handleRTEBlur = this.handleRTEBlur.bind(this)
}
/**
* Native React method
* that runs every time the component is about to receive new props.
*/
componentWillReceiveProps(nextProps) {
const languageChanged = this.props.local.use_lang != nextProps.local.use_lang;
if (languageChanged) {
// how do I unmount the CKEditor and remount it ???
console.log('use_lang changed');
}
}
handleRTEChange(evt) {
// keeps track of changes within the correct language section
}
handleRTEBlur() {
// fully updates the specs only on Blur
}
getContent () {
// gets content relative to current use language
}
render() {
const content = this.getContent();
// This is logging the content relative to the current language (as expected),
// but CKEditor doesn't show any changes when content changes.
console.log('content:', content);
// I need to find a way of unmounting and re-mounting CKEditor whenever use_lang changes.
return (
<div>
<CKEditor
content={content}
events={{
"blur": this.handleRTEBlur,
"change": this.handleRTEChange
}}
/>
</div>
)
}
}
Since CKEditor only uses "content" prop when it mounts, I needed to re-render the component when the local.use_lang of the parent component changes.
CKEditor can be forced to re-render by giving it a key prop equal to the value that should force it to re-render:
<CKEditor key={this.props.local.use_lang} etc />
This way, whenever the language prop changes, React creates a new CKEditor.
Keep in mind that I made use of this solution because CKEditor is a foreign component library that I installed with npm. If it was my own code I was working with, I would just make changes to how the Editor uses its props. However, since I refuse to make changes to foreign library code, this is the solution that allowed me to force a re-render without having to touch the internals of the Editor code.
Its because there is no change detected, so therefore it will not call render() again, and thus will not make another call to getContent().
What you can do is have the content be part of the state (which according to your constructor, already is), and check in componentWillReceiveProps() if the use_lang is updated. If so, then you can update the state there by calling this.setState({...rest, content = getContent()};.
And then your component render() function should look like this
<CKEditor
content={this.state.content}
events={{
"blur": this.handleRTEBlur,
"change": this.handleRTEChange
}}
/>
(As an aside, by calling setState(), this will in turn trigger a call to render(), and if any changes were detected, the changes will be shown. But note that this is not actually 'remounting' the component, it is merely updating the view. In other words, componentWillMount() and componentDidMount() will not be called after updating the state in this situation. Instead, componentWillUpdate() and componentDidUpdate() will be called). Read more about the component lifecycle here
I have this very simple Component, which is connected to redux state and returns {fruit, vegetables}. Everything works fine, but let's say I have a graph inside the Component and I if receive only updated vegetable from API the graph is being recreated each time.
Here's my component:
const Products = ({ fruit, vegetable }) =>
<div className='Products'>
<div>{vegetable.map(....logic here)}</div>
<div>{Math.random()}</div> // this is to illustrate the component is rendering every time
<Graph>Here will be a chart or a graph with fruit</Graph> //but it gets re-rendered even though there is no new fruit
</div>
const mapStateToProps = (state) => {
return {
fruit: state.data.fruit,
vegetable: state.data.vegetable,
}
}
export default connect(mapStateToProps)(Products)
It seems to me that every-time, no matter which states is updated it re-renders the whole components.
Is there a way to prevent that?
When a React component gets rendered, the whole tree of components below it also gets rendered - at the exception of the components which shouldComponentUpdate hook returns false. So in your case, if the Products component gets rendered, it is normal that the Graph component also does.
You have two options here:
if your Products component does not use the fruit prop outside of the Graph component, you can connect directly your Graph component to the fruitstate, and use the pure option of the connect function to avoid re-renders when fruit does not change
you can define the shouldComponentUpdate hook in your Graph component to manually skip unnecessary renders, or use a helper library to do it for you, for example the pure helper of the recompose library
The first option is where optimizing react/redux apps / avoiding unnecessary renders generally starts: connect your components to the store at the lowest level where it makes sense. The second option is more of an escape hatch - but still often useful.
As you mention you use stateless components, you can use a higher-order component to benefit from the shouldComponentUpdate hook. To understand how this works, here's how a simple implementation of it could look like this:
function pure(BaseComponent, shouldUpdateFn) {
return class extends Component {
shouldComponentUpdate(nextProps) {
return shouldUpdateFn(this.props, nextProps);
}
render() {
return <BaseComponent { ...this.props } />;
}
}
}
This would give you a pure HOC that you could reuse over your app to avoid unnecessary renders: it works by wrapping your stateless component into a new component with the desired hook. You'd use it like so, for example:
export default pure(Graph, (props, nextProps) => props.fruit !== nextProps.fruit)
Still, i highly encourage you in having a look at recompose, which has more fine-grained implementations of this, and would avoid you to reinvent the wheel.
To prevent a component to rerender when receiving new props, you can implement shouldcomponentupdate() in Graph.
Use shouldComponentUpdate() to let React know if a component's output is not affected by the current change in state or props. The default behavior is to re-render on every state change, and in the vast majority of cases you should rely on the default behavior.
shouldComponentUpdate() is invoked before rendering when new props or state are being received. Defaults to true. This method is not called for the initial render or when forceUpdate() is used.
Returning false does not prevent child components from re-rendering when their state changes.
Currently, if shouldComponentUpdate() returns false, then componentWillUpdate(), render(), and componentDidUpdate() will not be invoked. Note that in the future React may treat shouldComponentUpdate() as a hint rather than a strict directive, and returning false may still result in a re-rendering of the component.
If you determine a specific component is slow after profiling, you may change it to inherit from React.PureComponent which implements shouldComponentUpdate() with a shallow prop and state comparison. If you are confident you want to write it by hand, you may compare this.props with nextProps and this.state with nextState and return false to tell React the update can be skipped.
To help you implementing shouldComponentUpdate(), you can use eitherReact shallow-compare() or a custom shallow compare function
Given your current code.
React will update the whole component when state is changed.
So Graph Component will get updated.
If you don't want Graph Component to get updated you can add shouldComponentUpdate in your Graph Component and introduce checks there for re-rendering like as follows
shouldComponentUpdate: function(nextProps, nextState) {
// You can access `this.props` and `this.state` here
// and check them against nextProps and nextState respectively.
// return boolean(false) if you don't want the component to re-render.
}
Let's say I have a CookingClass component that gets initialized like this.
let teachers = makeTeachers(["Amber", "Jason", "Lily"])
let students = makeStudents(["Hopper"])
<CookingClass
teachers={teachers}
students={students}
/>
One of the teachers dropped out:
let newTeachers = makeTeachers(["Amber", "Jason"])
<CookingClass
teachers={newTeachers}
/>
It will make the entire component re-render. I am not sure whether React only calculates the diff and efficiently re-renders it or I must use shouldComponentUpdate to take care of it myself.
More real-world example might be implementing a Google map where there are a million markers and you want to replace one of the markers.
You're changing a so called Virtual DOM node. For every change in a virtual node shouldComponentUpdate() gets called. If you don't implement it yourself it will always return true;
So if you only want to reload your CookingClass in specific cases you would have to implement it yourself.
The pro of React is that it will only re-render Native DOM nodes when they will get changed in the Virtual DOM. This is the "render" which makes React so fast.
Based on your sample code, the component will re-render everytime.
You should use the react-redux (documentation) bindings to "connect" the component to the store.
// ConnectedCookingClass.js
import { connect } from 'react-redux';
import CookingClass from './CookingClass';
const mapStateToProps = (state) => {
return {
teachers: state.teachers,
students: state.students
};
};
const ConnectedCookingClass = connect(mapStateToProps)(CookingClass);
export default ConnectedCookingClass;
Then use this component elsewhere like so:
// OtherComponent.js
import ConnectedCookingClass from './ConnectedCookingClass';
const OtherComponent = React.createElement({
render() {
return (
<div>
<ConnectedCookingClass />
</div>
);
}
});
The react-redux bindings will do some smart things for you, like only re-rendering the component when the props returned by mapStateToProps are actually different than their previous value (via a shallowEqual comparison), so you should try to only return values here, no functions. Functions should be returned in mapDispatchToProps.
The default implementation of shouldComponentUpdate in react-redux will return true when:
ALWAYS if the component is a "pure" component (aka stateless-function)
When the props have been updated manually (after componentWillReceiveProps called)
When the store has changed and the new props are different than the old props.
Here's what that looks like from the source code:
shouldComponentUpdate() {
return !pure || this.haveOwnPropsChanged || this.hasStoreStateChanged
}
The real DOM Rendering is completely handled by React with very efficient innerHTML inserts and only for changes in the new data structure of your application VirtualDomTree.
shouldComponentUpdate() controls if the component should be recalculated or not. You should use it, when you are rendering statical data, for example. The output of the component will not change, so you could just return false and the first version of the component will be used for ever ;)