Improve Performance in React Class Components using React.PureComponent

March 20, 2019 0 Comments

Improve Performance in React Class Components using React.PureComponent

 

 

At the release of React v16.6, it heralded great advancements on how we use Reactjs. One of the great additions to React v16.6 is the React.PureComponent class.

In this post, we will look in depth at what React.PureComponent and the goodies it brings us. Read on.

Tip: Building with components? you should probably make them reusable to share across apps. Open-source tools like Bit can help you out, take a look:

We used to write our components like this:

import React from 'react';
class Count extends React.Component {
constructor(props) {
super(props)
this.state = {
count: 0
}
}
render() {
return (
<div >
<h1> Count Component { this.state.count } </h1>
<input onChange = {
(evt) => {
console.log(evt.target.value)
const val = evt.target.value
if(val ! "")
this.setState({ count: val })
}
} placeholder = "Enter a Count..." />
</div>
)
}
}
export default Count;

Components have a render method which returns the JSX markup that it renders to the DOM. For change detection, React use a local state, which is only local to a component, when the state changes the component and its children are re-rendered to update the UI of the changed state. Then we have the props which is a short form of properties for passing attributes to components, it is received in the constructor as this.props.

Now, this component has a count state with an initial value of 0, when we enter a number in the input box the onchange event is fired, the current value in the input box is used to update the count state. If we enter 1, the count state is updated to 1, when we enter 234, the count state will be updated to 234.</p><figure id="63d6" class="graf graf--figure graf-after--p"><img class="progressiveMedia-noscript js-progressiveMedia-inner" src="https://cdn-images-1.medium.com/max/1600/1*9KflfFyEBdk73bd5wYtDFw.gif"></figure><p id="f895" class="graf graf--p graf-after--figure">When we enter a certain number and re-enter the same number again. Should the DOM of the component re-rendered? No, of course not, that will lead to slowdowns in our apps.</p><p id="c61c" class="graf graf--p graf-after--p">But here in our component, the component will be re-rendered even if the previous state and the current state is the same value.</p><figure id="1448" class="graf graf--figure graf-after--p"><img class="progressiveMedia-noscript js-progressiveMedia-inner" src="https://cdn-images-1.medium.com/max/1600/1*PCBcC40bbKCrn0Y4-vgrdA.gif"></figure><p id="7ce9" class="graf graf--p graf-after--figure">To visualize this re-renders with React Devs Tools. Open your DevsTools and click on the React tab</p></div><div class="section-inner sectionLayout--outsetColumn"><figure id="f630" class="graf graf--figure graf--layoutOutsetCenter graf-after--p"><img class="progressiveMedia-noscript js-progressiveMedia-inner" src="https://cdn-images-1.medium.com/max/2400/1*xNXLSiU8TPhE43RJGQ-Szw.jpeg"></figure></div><div class="section-inner sectionLayout--insetColumn"><p id="fb79" class="graf graf--p graf-after--figure">click on the <strong class="markup--strong markup--p-strong">HighLight Updates</strong> check-box. If the <strong class="markup--strong markup--p-strong">HighLight Updates</strong> check-box is not available, click on the Settings box on the right side, a popup will appear, then click on <strong class="markup--strong markup--p-strong">HighLight Updates</strong>.</p><p id="cbde" class="graf graf--p graf-after--p">If we now interact with our app, we will see color highlights momentarily appear around any components that render or re-render.</p><p id="d7e7" class="graf graf--p graf-after--p">Considering the component above, if we click the input and type in 1 the component will re-render to reflect the value 1 on the DOM. We will see the borders around the Count component flash color(blue in my machine).</p><figure id="3c0b" class="graf graf--figure graf-after--p"><img class="progressiveMedia-noscript js-progressiveMedia-inner" src="https://cdn-images-1.medium.com/max/1600/1*man-46fFdA4qLKxRchUZPQ.gif"></figure><p id="a498" class="graf graf--p graf-after--figure">Now, if we type 2 there will be a flash, then we type 2 again. there shouldn&#x2019;t be a flash but it occurred.</p><figure id="c3e4" class="graf graf--figure graf-after--p"><img class="progressiveMedia-noscript js-progressiveMedia-inner" src="https://cdn-images-1.medium.com/max/1600/1*mJgIa7a_xLM-ORR8nKL27g.gif"></figure><p id="4adf" class="graf graf--p graf-after--figure">We know this is unnecessary because the previous value didn&#x2019;t change. We know this is a small app and we can do with the re-renderings, but imagine a complex app with thousands of re-renders imagine how sluggish your app will look, you see the slowdowns will become noticeable. We can throw a little salt in our app to boost its performance by adding the lifecycle method, <strong class="markup--strong markup--p-strong">shouldComponentUpdate</strong>.</p><p id="0092" class="graf graf--p graf-after--h3">This method tells React whether to update a component or not, if it is to update a component it returns true, if not it returns false.</p><pre id="1f7a" class="graf graf--pre graf-after--p">function shouldComponentUpdate(nextProps, nextState) {<br> //...<br>}</pre><p id="8454" class="graf graf--p graf-after--pre">The basic skeleton of the function, it takes the next props values in the nextProps parameter and the value of the next state in the nextState parameter.</p><p id="26b4" class="graf graf--p graf-after--p">Applying this to our Count component:</p><pre id="b011" class="graf graf--pre graf-after--p">class Count extends React.Component {<br> constructor() {<br> this.state = {<br> count: 0<br> }<br> }</pre><pre id="1e6b" class="graf graf--pre graf-after--pre"> shouldComponentUpdate(nextProps, nextState) {<br> if(nextState.count !== this.state.count) {<br> return true<br> }<br> return false<br> }</pre><pre id="5321" class="graf graf--pre graf-after--pre"> render() {<br> return ( <br> &lt;div &gt;<br> &lt;h1&gt; Count Component { this.state.count } &lt;/h1&gt; <br> &lt;input onChange = {<br> (evt) =&gt; {<br> console.log(evt.target.value)<br> const val = evt.target.value<br> if(val !== &quot;&quot;)<br> this.setState({ count: val })<br> }<br> } placeholder = &quot;Enter a Count...&quot; /&gt;<br> &lt;/div&gt;<br> )<br> }<br>}</pre><p id="fb92" class="graf graf--p graf-after--pre">We have now regulated the updates to occur when there is a new value. In the above implementation, we checked if the value of the <code class="markup--code markup--p-code">count</code> property in the next state is not the same as the one currently in the <code class="markup--code markup--p-code">count</code> property. If they are not the same we tell React to render by returning true, if not we tell React not to re-render by returning false.</p><p id="aba6" class="graf graf--p graf-after--p">Looking at our React DevTools, we will see that when we type in 2 there will be a flash and type it in again they will be no color flashing around the Count component&#xA0;:) this shows no re-rendering.</p><figure id="1071" class="graf graf--figure graf-after--p"><img class="progressiveMedia-noscript js-progressiveMedia-inner" src="https://cdn-images-1.medium.com/max/1600/1*3fMZ8UyxeYkoFSjdSXlJDg.gif"></figure><p id="91fc" class="graf graf--p graf-after--figure">Now, instead of writing shouldComponentUpdate() by hand, you can inherit from React.PureComponent.</p><p id="04d7" class="graf graf--p graf-after--h3">Instead of writing shouldComponent method in our components, React introduced a new Component with built-in shouldComponentUpdate implementation it is the <code class="markup--code markup--p-code">React.PureComponent</code> component.</p><blockquote id="f25d" class="graf graf--blockquote graf-after--p"><em class="markup--em markup--blockquote-em">It (</em><code class="markup--code markup--blockquote-code"><em class="markup--em markup--blockquote-em">React.PureComponent</em></code><em class="markup--em markup--blockquote-em">) is equivalent to implementing shouldComponentUpdate() with a shallow comparison of current and previous props and state. - </em><a href="https://gist.github.com/philipszdavido/cd11cadf9a079c61eca794285e7a46b5" class="markup--anchor markup--blockquote-anchor"><em class="markup--em markup--blockquote-em">Reactjs Blog</em></a></blockquote><p id="680f" class="graf graf--p graf-after--blockquote">What this means that we can simply remove the shouldComponentUpdate and make our component <code class="markup--code markup--p-code">extend React.PureCompoent</code> that it has already implemented the shouldComponentUpdate for us internally with shallow comparison (we will come to that).</p><p id="b34d" class="graf graf--p graf-after--p">So, now we edit our Count component to extend React,PureComponent:</p><pre id="8c86" class="graf graf--pre graf-after--p">class Count extends React.PureComponent {<br> constructor() {<br> this.state = {<br> count: 0<br> }<br> }</pre><pre id="c4b9" class="graf graf--pre graf-after--pre"> /*shouldComponentUpdate(nextProps, nextState) {<br> if(nextState.count !== this.state.count) {<br> return true<br> }<br> return false<br> }*/</pre><pre id="5489" class="graf graf--pre graf-after--pre"> render() {<br> return ( <br> &lt;div &gt;<br> &lt;h1&gt; Count Component { this.state.count } &lt;/h1&gt; <br> &lt;input onChange = {<br> (evt) =&gt; {<br> console.log(evt.target.value)<br> const val = evt.target.value<br> if(val !== &quot;&quot;)<br> this.setState({ count: val })<br> }<br> } placeholder = &quot;Enter a Count...&quot; /&gt;<br> &lt;/div&gt;<br> )<br> }<br>}</pre><p id="d6c9" class="graf graf--p graf-after--pre">Notice we commented out the shouldComponentUpdate method. Now, if look at our React DevTools you will see that if we type in 4 and type it in again there will be no flash.</p><figure id="a65c" class="graf graf--figure graf-after--p"><img class="progressiveMedia-noscript js-progressiveMedia-inner" src="https://cdn-images-1.medium.com/max/1600/1*rVIspuLA4n5q312lCRwjKA.gif"></figure><p id="d6cc" class="graf graf--p graf-after--figure"><em class="markup--em markup--p-em">claps</em></p><figure id="6d84" class="graf graf--figure graf-after--p"><img class="progressiveMedia-noscript js-progressiveMedia-inner" src="https://cdn-images-1.medium.com/max/1600/1*kGrZpdxO1u5l_RsD1j4WYw.gif"></figure><p id="0caa" class="graf graf--p graf-after--figure">React.PureComponent always does a shallow comparison of values, we will run into problems if we have a complex data structure like Arrays and Objects. You see we can&#x2019;t use it if the props or state may have been mutated in a way that a shallow comparison would miss.</p><p id="3ffb" class="graf graf--p graf-after--p">Let&#x2019;s edit our Count component to include a complex data structure:</p><pre id="6cf7" class="graf graf--pre graf-after--p">import React from &apos;react&apos;;</pre><pre id="4aad" class="graf graf--pre graf-after--pre">class Count extends React.PureComponent {<br> constructor(props) {<br> super(props)<br> this.state = {<br> count: [0]<br> }<br> }<br> componentDidUpdate(prevProps, prevState) {<br> console.log(&quot;componentDidUpdate&quot;)<br> }<br> componentWillUpdate(nextProps, nextState) {<br> console.log(&quot;componentWillUpdate&quot;)<br> }<br> /*shouldComponentUpdate(nextProps, nextState) {<br> if(nextState.count !== this.state.count) {<br> return true<br> }<br> return false<br> }*/</pre><pre id="612f" class="graf graf--pre graf-after--pre"> render() {<br> return ( <br> &lt;div &gt;<br> &lt;h1&gt; Count Component { this.state.count } &lt;/h1&gt; <br> &lt;input onChange = {<br> (evt) =&gt; {<br> console.log(evt.target.value)<br> const val = evt.target.value<br> if(val !== &quot;&quot;){<br> const count = this.state.count<br> count.push(val)<br> this.setState({ count: count })<br> <br> }<br> }<br> } placeholder = &quot;Enter a Count...&quot; /&gt;<br> &lt;/div&gt;<br> )<br> }<br>}</pre><pre id="67b4" class="graf graf--pre graf-after--pre">export default Count;</pre><p id="4d07" class="graf graf--p graf-after--pre">This code won&#x2019;t trigger a re-render</p><figure id="3d9e" class="graf graf--figure graf-after--p"><img class="progressiveMedia-noscript js-progressiveMedia-inner" src="https://cdn-images-1.medium.com/max/1600/1*oy-bY2k2CN2ifTj2Ej2aAQ.gif"></figure><p id="e605" class="graf graf--p graf-after--figure">upon the value of the <code class="markup--code markup--p-code">count</code> property changed. The problem comes from that React.PureComponent does a shallow comparison of the state values.</p><p id="6a3e" class="graf graf--p graf-after--p">Let&#x2019;s say we entered 4. The prev value of count will be [0] while the next value will be [0, 4] but the component didn&#x2019;t re-rendered yet the two values are different. The problem is from mutation of data.</p><p id="73d4" class="graf graf--p graf-after--h3">Mutation has been a very big issue in JS. Mutating data in Js might lead to distrust that the data may have been changed somewhere in the app by unlikely sources. Best practices always tell us not to mutate data in our app.</p><p id="87a4" class="graf graf--p graf-after--p">As PureComponeny does shallowCompare it uses the equality operator to check for sameness in values. Now, if we mutate our arrays using the push method. The === equality operator doesn&#x2019;t detect a change in the address of the array.</p><p id="170a" class="graf graf--p graf-after--p"><strong class="markup--strong markup--p-strong">Don&#x2019;t understand?</strong></p><p id="12a1" class="graf graf--p graf-after--p">Now, data structures are stored by their references to their memory addresses. All what === does is to check the memory address for sameness.</p><pre id="9a3b" class="graf graf--pre graf-after--p">let a = [90]<br>let b = [90]</pre><pre id="1812" class="graf graf--pre graf-after--pre"><code class="markup--code markup--pre-code">0 | | 14 | |<br>1 | | 15 | |<br>2 | | 16 | |<br>3 | a | 17 | |<br>4 | b | 18 | |<br>5 | | 19 | |<br>6 | | 20 | |<br>7 | | 21 | 90 |<br>8 | | 22 | |<br>9 | | 23 | |<br>10 | | 24 | |<br>11 | 90 | 25 | |<br>12 | | 26 | |<br>13 | | 27 | |</code></pre><p id="6739" class="graf graf--p graf-after--pre">a and b refers to different memory address. a might refer to cell #0011 and b might point to cell #0021. When we do:</p><pre id="8f6b" class="graf graf--pre graf-after--p">a.push(88)</pre><p id="8ff7" class="graf graf--p graf-after--pre">The push method doesn&#x2019;t create another memory to store 90 and the pushed 88. It goes to the memory pointed to by a and updates the values</p><pre id="fd06" class="graf graf--pre graf-after--p"><code class="markup--code markup--pre-code">0 | | 14 | |<br>1 | | 15 | |<br>2 | | 16 | |<br>3 | a-&gt; 11 | 17 | |<br>4 | b-&gt; 21 | 18 | |<br>5 | p -&gt; 11 | 19 | |<br>6 | | 20 | |<br>7 | | 21 | 90 |<br>8 | | 22 | |<br>9 | | 23 | |<br>10 | | 24 | |<br>11 | 90 | 25 | |<br>12 | 88 | 26 | |<br>13 | | 27 | |</code></pre><p id="8859" class="graf graf--p graf-after--pre">Here now, a has been mutated meaning that the nature/state/structure of it has been altered(just like changing the base pairs of a DNA).</p><p id="9298" class="graf graf--p graf-after--p">The address of a stills points to 11, === doest check the values stored in the address it checks whether the address of the LHS array is the same with the address of RHS.</p><pre id="fee5" class="graf graf--pre graf-after--p">let a = [90]<br>let p = a<br>let b = [90]<br>a.push(88)<br>log(a===p)</pre><pre id="94fa" class="graf graf--pre graf-after--pre">// true</pre><p id="2f83" class="graf graf--p graf-after--pre">If we use a non-mutating method like concat</p><pre id="1a21" class="graf graf--pre graf-after--p">let a = [90]<br>let p = a<br>let b = [90]<br>p = a.concat([88])<br>log(a===p)</pre><pre id="a895" class="graf graf--pre graf-after--pre">// false</pre><p id="f445" class="graf graf--p graf-after--pre">concat creates a new array in memory and appends the value and returns the new array.</p><pre id="6e3d" class="graf graf--pre graf-after--p"><code class="markup--code markup--pre-code">0 | | 14 | |<br>1 | | 15 | |<br>2 | | 16 | |<br>3 | a-&gt; 11 | 17 | |<br>4 | b-&gt; 21 | 18 | |<br>5 | p-&gt; 25 | 19 | |<br>6 | | 20 | |<br>7 | | 21 | 90 |<br>8 | | 22 | |<br>9 | | 23 | |<br>10 | | 24 | |<br>11 | 90 | 25 | 90 |<br>12 | | 26 | 88 |<br>13 | | 27 | |</code></pre><p id="b362" class="graf graf--p graf-after--pre"><code class="markup--code markup--p-code">a</code> points to 11, p points to 25 two different memory addresses. We see concat doesn&apos;t touch the old array, it creates a new one and returns it leaving the original one unchanged.</p><p id="c0ae" class="graf graf--p graf-after--p">So we see with this we can detect that something changed in the values because of the change in the memory addresses.</p><p id="a9cc" class="graf graf--p graf-after--p">This is also evident in their C++ impl. in v8:</p><pre id="159c" class="graf graf--pre graf-after--p">BUILTIN(ArrayPush) {<br> HandleScope scope(isolate);<br> Handle&lt;Object&gt; receiver = args.receiver();<br> // ...<br> Handle&lt;JSArray&gt; array = Handle&lt;JSArray&gt;::cast(receiver);<br> //...<br> ElementsAccessor* accessor = array-&gt;GetElementsAccessor();<br> int new_length = accessor-&gt;Push(array, &amp;args, to_add);<br> return Smi::FromInt(new_length);<br>}</pre><pre id="1108" class="graf graf--pre graf-after--pre">BUILTIN(ArraySplice) {<br> HandleScope scope(isolate);<br> Handle&lt;Object&gt; receiver = args.receiver();<br> // ...<br> Handle&lt;JSArray&gt; array = Handle&lt;JSArray&gt;::cast(receiver);<br> Handle&lt;JSArray&gt; result_array = accessor-&gt;Splice(<br> array, actual_start, actual_delete_count, &amp;args, add_count);<br> return *result_array;<br>}</pre><p id="16e0" class="graf graf--p graf-after--pre">The JSArray is the C++ class that describes JS arrays. See in Push: The JSArray is cast from the receiver, then The value is pushed to the JSArray by calling <code class="markup--code markup--p-code">acessor-&gt;Push(...)</code> and a new length is returned. In the Splice: see a new array <code class="markup--code markup--p-code">Handle&lt;JSArray&gt; result_array</code> is returned.</p><p id="f3e5" class="graf graf--p graf-after--p">Now we have seen in entirety how mutation affects equality check. To tell the equality check that your data structure changed we need to return a new array, not to mutate/change the structure of the original data. We see this practice in Redux, whereby the reducers are pure functions and always return new data structure, if no addition or subtraction action is caught, the state is returned as it is without changing the structure:</p><pre id="6132" class="graf graf--pre graf-after--p">function reducer (state = initialState, action) {<br> switch(action) {<br> case &quot;ADD_ACTION&quot;:<br> // return new state using spread<br> return [...state, action.payload]<br> case &quot;REMOVE_ACTION&quot;:<br> // returning a new array using the non-mutating splice method<br> return state.splice()<br> default:<br> // no data change return the same state<br> return state<br> }<br>}</pre><p id="6cba" class="graf graf--p graf-after--pre">We can see the beauty and intelligence behind it. If the state ever changes a new state is returned, if no change is made to the state the same is simply returned so there will be no re-render. That&#x2019;s the reason react-redux is the most efficient way to trigger and control unnecessary re-renders in React.</p><p id="35e1" class="graf graf--p graf-after--p">Coming back to our Count component, we need to remove the mutating push method and update the count array with a non-mutating method which returns a new array.</p><pre id="0154" class="graf graf--pre graf-after--p">import React from &apos;react&apos;;</pre><pre id="e41f" class="graf graf--pre graf-after--pre">class Count extends React.PureComponent {<br> constructor(props) {<br> super(props)<br> this.state = {<br> count: [0]<br> }<br> }<br> componentDidUpdate(prevProps, prevState) {<br> console.log(&quot;componentDidUpdate&quot;)<br> }<br> componentWillUpdate(nextProps, nextState) {<br> console.log(&quot;componentWillUpdate&quot;)<br> }<br> /*shouldComponentUpdate(nextProps, nextState) {<br> if(nextState.count !== this.state.count) {<br> return true<br> }<br> return false<br> }*/</pre><pre id="cd3a" class="graf graf--pre graf-after--pre"> render() {<br> return ( <br> &lt;div&gt;<br> &lt;h1&gt; Count Component { this.state.count } &lt;/h1&gt; <br> &lt;input onChange = {<br> (evt) =&gt; {<br> console.log(evt.target.value)<br> const val = evt.target.value<br> if(val !== &quot;&quot;){<br> this.setState({ count: this.state.count.concat([val]) })<br> }<br> }<br> } placeholder = &quot;Enter a Count...&quot; /&gt;<br> &lt;/div&gt;<br> )<br> }<br>}</pre><pre id="dffb" class="graf graf--pre graf-after--pre">export default Count;</pre><p id="8825" class="graf graf--p graf-after--pre">We used the concat method instead of push. It returns a new array, so when the equality runs on it, the two states will have different memory references and it will trigger a re-render.</p><figure id="f569" class="graf graf--figure graf-after--p"><img class="progressiveMedia-noscript js-progressiveMedia-inner" src="https://cdn-images-1.medium.com/max/1600/1*EBRgrcz7C0zVmtHJJxnDeQ.gif"></figure><p id="8bfb" class="graf graf--p graf-after--h3">You can find a similar thing in Angular: OnPush strategy not really the same, but an Angular component with the OnPush Strategy is only rendered on the first rendering of the app, on the next renderings it won&#x2019;t be rendered by Angular, in fact, it will be skipped.</p><p id="9cbc" class="graf graf--p graf-after--p">It will only be run when:</p><ul class="postList"><li id="a054" class="graf graf--li graf-after--p">An async events occur (only DOM events) because DOM events will surely cause a change in the component&#x2019;s properties.</li><li id="bf21" class="graf graf--li graf-after--li">By the dev by injecting the ChangeDetectorRef class and calling the <code class="markup--code markup--li-code">markForCheck</code>.</li><li id="a874" class="graf graf--li graf-after--li">Only when the inputs(properties) of the component has changed.</li></ul><p id="dafd" class="graf graf--p graf-after--li">You see it is like React.PureComponent, OnPush checks for unnecessary re-renders.</p><p id="f9e0" class="graf graf--p graf-after--p">If we port our React Count component to Angular it will look like this:</p><pre id="19d3" class="graf graf--pre graf-after--p">import { Component, OnInit } from &apos;@angular/core&apos;;<br>@Component({<br> selector: &apos;app-count&apos;,<br> template:
<div>
{{viewRun}}
<h1> Count Component {{count}} </h1>
<input (change)="onChange($event)" placeholder="Enter a Count..." />
</div>
<br>})<br>export class Count implements OnInit {<br> count = 0<br> onChange(evt) {<br> console.log(evt.target.value)<br> const val = evt.target.value<br> if(val !== &quot;&quot;)<br> this.count= val<br> }</pre><pre id="e239" class="graf graf--pre graf-after--pre"> ngOnInit() {}</pre><pre id="715a" class="graf graf--pre graf-after--pre"> get viewRun() {<br> console.log(&apos;view updated:&apos;, this.count)<br> return true<br> }<br>}</pre><p id="72da" class="graf graf--p graf-after--pre">Here, if we type in a number like 6, there will be an update, <code class="markup--code markup--p-code">view updated: 6</code> will be logged in the screen. If we clear the number 6 and retype 6, <code class="markup--code markup--p-code">view updated: 6</code> will be logged again in the screen. The old value is 6 and the new value is 6, there is no suppose to be an update because the old value and new value is the same.</p><p id="6457" class="graf graf--p graf-after--p">You see that is the same that we encountered in our React version. Angular doesn&#x2019;t have React&#x2019;s shouldComponentUpdate lifecycle hook equivalent to tell Angular when to update the component. But rather have it in its OnPush CD strategy.</p><pre id="eb93" class="graf graf--pre graf-after--p">import { Component, OnInit, ChangeDetectionStrategy } from &apos;@angular/core&apos;;</pre><pre id="ab8c" class="graf graf--pre graf-after--pre">@Component({<br> selector: &apos;app-count&apos;,<br> template:
<div>
{{viewRun}}
<h1> Count Component {{count}} </h1>
<input (change)="onChange($event)" placeholder="Enter a Count..." />
</div>
,<br> changeDetection: ChangeDetectionStrategy.OnPush<br>})<br>export class Count implements OnInit {<br> count = 0<br> onChange(evt) {<br> console.log(evt.target.value)<br> const val = evt.target.value<br> if(val !== &quot;&quot;)<br> this.count= val<br> }<br> ngOnInit() { }<br> get viewRun() {<br> console.log(&apos;view updated:&apos;, this.count)<br> return true<br> }<br>}</pre><p id="60a3" class="graf graf--p graf-after--pre">When an input is changed, a CD run is triggered. On every OnPush components, Angular checks if the old values and the new values are different before allowing the CD run on the OnPush component. If the old values and the new values are the same, Angular cancels the CD on the OnPush components. This is evident in our Count component above which is now an OnPush component. If we type in 99, there will be an update run on the component, if we clear the number and enter it again, there will be no update.</p><p id="9dcf" class="graf graf--p graf-after--p">So, OnPush makes the component &#x201C;pure&#x201D; just like React.PureComponent, no wasted renders.</p><p id="3fbf" class="graf graf--p graf-after--p">Angular&#x2019;s change detection mechanism is the same as React.PureComponent they both use shallow compare. It uses the equality check <code class="markup--code markup--p-code">===</code> to check for sameness.</p><pre id="dff5" class="graf graf--pre graf-after--p">// angular/src/util.ts<br>function looseIdentical(a: any, b: any) {<br> return a === b<br>}</pre><p id="7449" class="graf graf--p graf-after--pre">This is the function called by Angular during change detection run to check for changes.</p><p id="b2b0" class="graf graf--p graf-after--p">So, we should also be aware of mutation in Angular OnPush components because the <code class="markup--code markup--p-code">===</code> equality operator checks for reference changes in data structures.</p><pre id="da57" class="graf graf--pre graf-after--p">// ...<br>@Component({<br> selector: &apos;app-count&apos;,<br> template:
<div>
{{viewRun}}
<h1> Count Component {{count}} </h1>
<input (change)="onChange($event)" placeholder="Enter a Count..." />
</div>
,<br> changeDetection: ChangeDetectionStrategy.OnPush<br>})<br>export class Count implements OnInit {<br> count = []<br> onChange(evt) {<br> console.log(evt.target.value)<br> const val = evt.target.value<br> if(val !== &quot;&quot;)<br> this.count.push(val)<br> }<br> ngOnInit() { }<br> get viewRun() {<br> console.log(&apos;view updated:&apos;, this.count)<br> return true<br> }<br>}</pre><p id="d9f1" class="graf graf--p graf-after--pre">The above will not trigger a CD run on the component when a number is entered in the input box.</p><p id="d25d" class="graf graf--p graf-after--p"><strong class="markup--strong markup--p-strong">Why?</strong> because the new value reference will still point to the old value.</p><p id="27ea" class="graf graf--p graf-after--p">We should always return new data structures or use methods or practices that don&#x2019;t mutate the original data. To make the component re-render when a new value is added to the count array we will use the <code class="markup--code markup--p-code">concat</code> method which returns a new array on each call:</p><pre id="b728" class="graf graf--pre graf-after--p">// ...<br>@Component({<br> selector: &apos;app-count&apos;,<br> template:
<div>
{{viewRun}}
<h1> Count Component {{count}} </h1>
<input (change)="onChange($event)" placeholder="Enter a Count..." />
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class Count implements OnInit {
count = []
onChange(evt) {
console.log(evt.target.value)
const val = evt.target.value
if(val ! "")
this.count = this.count.concat([val])
}
ngOnInit() { }
get viewRun() {
console.log('view updated:', this.count)
return true
}

}

Let’s peep into the React source to see how this works internally.

First, we look at where our base components are defined: react-master\packages\react\src\ReactBaseClasses.js

// react/src/ReactBaseClasses.js
// ...
/**
* Convenience component with default shallow equality check for sCU.
*/
function PureComponent(props, context, updater) {
// ...
}
const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
// Avoid an extra prototype jump for these methods.
Object.assign(pureComponentPrototype, Component.prototype);
pureComponentPrototype.isPureReactComponent = true;
export {Component, PureComponent};

We can see the PureComponent above, the interesting part is the isPureReactComponent property, it denotes that the component is a pure component. Let's look at what happens when a component is checked for updating:

function checkShouldComponentUpdate(
workInProgress,
ctor,
oldProps,
newProps,
oldState,
newState,
nextContext,
) {
const instance = workInProgress.stateNode;
if (typeof instance.shouldComponentUpdate === 'function') {
startPhaseTimer(workInProgress, 'shouldComponentUpdate');
const shouldUpdate = instance.shouldComponentUpdate(
newProps,
newState,
nextContext,
);
stopPhaseTimer();
    if (DEV) {
//...
}
    return shouldUpdate;
}
  if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return (
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
);
}
  return true;
}

This function is where the shouldComponentUpdate method is called in React components. It first checks if the function exists and is a function, next it calls it passing in the next props and next state values. The return value is stored in shouldUpdate and returned.

Looking down we see where a check for PureComponet is made. See the isPureComponent property we saw earlier is used to check if the component is a PureComponent. If yes, the built-in comparison method shallowEqual is called. The function is shallowCompare, the function that is called on all PureComponents, remember PureComponents leaves us without the need to add shouldComponentUpdate check because it been run for us in the shallowCompare function.

We looked into so many concepts in this post:

  • What mutation entails
  • Function purity
  • Performance increment the use of sholudComponentUpdate
  • Performance increment the use of React.PureComponent

The performance boost that React.PureComponent brings to the table is freaking awesome.

If you have any question regarding this or anything I should add, correct or remove, feel free to comment, email or DM me.

Thanks !!!


Tag cloud