ReactJS in Simple English — state (2)

Johnny Lai
Analytics Vidhya
Published in
8 min readNov 16, 2020

--

This time we would go a little bit deeper about state, including shouldComponentUpdate(), PureComponent and Context API

Before we continue, let’s have a look How ReactJS update our pages

  1. Detected changes in props or state
  2. Trigger shouldComponentUpdate(), by default return true, that means it always go to the next step render()
  3. render() is called to render the new virtual DOM
  4. Compare the old virtual DOM with the newly rendered virtual DOM
  5. Update the real DOM if any difference found, only update the parts which are different instead of the whole DOM

By default, if any changes in props or state, React will always render() the new virtual DOM including all components and compare it with the last version.
For example, user just click the submit button and trigger the modal panel and confirm dialog. Obviously, this is a small changes in screen, probably a boolean in state indicated the confirm dialog have to be shown, nothing related with other components.
However, React will still render a virtual DOM including all components and check against with the old version. Think about if we have a large application and every single change will trigger the same behavior - render and check again all components. It would spend a lot of resources and slowdown the application.

In order to enhance the performance, we can implement some checking in shouldComponentUpdate() or apply PureComponent in order to skip some unnecessary render() and speed up the checking (as the new rendered DOM will including less components)

  1. shouldComponentUpdate()

It is one of the lifecycle hooks in React, by default return true, that means it always go to the next step render(). We can implement some checking here, so if the state / props didn’t changed in that class, then we don’t include in the coming rendering.

Let’s the example for better understanding. In this example, we have 3 class. App contains PureInventory and Inventory

App class initialize the state with three variables — counters for App/ PureInventory/ Inventory and the functions to add the counters for App/ PureInventory/ Inventory, and we pass them through props

In the render method, it includes both children — PureInventory and Inventory, and a clock — whenever the render trigger, the clock updates

//App.jsclass App extends Component {state = {
counter: 0,
childCounter: 0,
pureChildCounter: 0
};
increaseCounterHandler = () => {
this.setState((state) => {
return { counter: state.counter + 1 };
});
};
increaseChildCounterHandler = () => {
this.setState((state) => {
return { childCounter: state.childCounter + 1 };
});
};
increasePureChildCounterHandler = () => {
this.setState((state) => {
return { pureChildCounter: state.pureChildCounter + 1 };
});
};
render() {
console.log("parent render");
return (
<div className="App">
<button onClick={this.increaseCounterHandler}>
Increase App Counter
</button>
<p>App Counter {this.state.counter}</p>
<p>Last Update {new Date().toLocaleString()}</p>
<div>
<Inventory
clicked={this.increaseChildCounterHandler} childCounter= {this.state.childCounter}/>
<PureInventory
clicked={this.increasePureChildCounterHandler} pureChildCounter={this.state.pureChildCounter}/>
</div>
</div>
);
}
}

PureInventory class, a very simple class, have a button to increase its counter and a clock — whenever the render trigger, the clock updates

//PureInventory.jsclass PureInventory extends Component {render() {
console.log("pure children render");
return (
<div>
<button onClick={this.props.clicked}> Increase PureInventory Counter </button>
<p>PureInventory Counter {this.props.pureChildCounter}</p> <p>Last Update {new Date().toLocaleString()}</p>
</div>
);
}
}

Inventory class, similar with PureInventory class, have a button to increase its counter and a clock — whenever the render trigger, the clock updates

class Inventory extends Component {render() {
console.log("children render");
return (
<div>
<button onClick={this.props.clicked}>Increase Inventory Counter</button>
<p>Inventory Counter {this.props.childCounter}</p>
<p>Last Update {new Date().toLocaleString()}</p>
</div>
);
}
}

As mentioned, by default (if you haven’t implement shouldComponentUpdate() / PureComponent), any changes in the state will trigger the render() (of all class)to re-render everything again in the new virtual DOM

App counter increase, both child — Inventory and PureInventory re-render too, as the time for last update are same
Inventory counter increase, both Parent and PureInventory re-render too, as the time for last update are same
Pure Child counter increase, both Parent and Child re-render too, as the time for last update are same

In order to fix this, we can implement shouldComponentUpdate(nextProps, nextState) in PureInventory Class - nextProps means the changed props, nextState means the changed state.
So, we can compare the current props/state with the changes props/ state.
In this case, PureInventory only got 1 props — pureChildCounter and we compare the new props and old props by !==, that means only go further (render()) if the new pureChildCounter is different from the current pureChildCounter.

//PureInventory.jsclass PureInventory extends Component {shouldComponentUpdate(nextProps, nextState) {
return
this.props.pureChildCounter !== nextProps.pureChildCounter;
}
render() {
console.log("pure children render");
return (
<div>
<button onClick={this.props.clicked}> Increase Pure Child Counter </button>
<p>Pure Child Counter {this.props.pureChildCounter}</p> <p>Last Update {new Date().toLocaleString()}</p>
</div>
);
}
}
After implemented shouldComponentUpdate() in PureInventory Class, it no longer re-render again if the increase of counter is in App or Inventory (not related with PureInventory)

Let try the working example

2. PureComponent

PureComponent is very similar with shouldComponentUpdate(), except that PureComponent will check all the props / state but you can customize it in shouldComponentUpdate()

Implement PureComponent is very simple, just import it and extends “PureComponent” instead of “Component”, other things are same

import React, { PureComponent } from "react";class PureInventory extends PureComponent {render() {   console.log("pure children render");
return (
<div>
<button onClick={this.props.clicked}>
Increase Pure Child Counter
</button>
<p>Pure Child Counter {this.props.pureChildCounter}</p>
<p>Last Update {new Date().toLocaleString()}</p>
</div>
);
}
}
export default PureInventory;

Let’s try the working example

When to use shouldComponentUpdate() / PureComponent?

It depends, if there are some components always update with parent, then implement any checking on them is a waste of resources. For some cases, you very sure that shouldn’t involved with other components, like what we mentioned in the beginning — the confirm dialog with modal panel, shouldComponentUpdate() will be suitable, because you exactly understand what should be check. If the application is complex, PureComponent will be better, saving implementation and testing time, less chance to make mistake.

How shouldComponentUpdate() / PureComponent compare the props/ state?

PureComponent implements a shallow comparison, that means it would compare the data itself if that is a primitive value. However, if it is an object, it would compare the object reference instead of the values in object.
In shouldComponentUpdate(), you can implement anything you want, but if you implement deep comparison here, it may slow down your application.
That’s why in last passage, I emphasis we “replace” the state but not edit the data inside.

3. Context API

In last passage, we walked though the basic about state, we can pass state to another class / components by props. Think about if we have a large application, from top to bottom, it may have several layers, if we want to pass a prop, we have to go through every layer which require a lot of unnecessary coding and extra time to maintain it.

Let’s have a look this example, App contains Layout, and Layout contains Menu and Profile. If we want to pass a prop from App to Menu and Profile, then we have to go through Layout too.

Top Level — App.js

//App.js
class App extends Component {
state = {
adminMenu: false,
customerMenu: true,
name: "Fifi",
position: "Customer Service Operator"
};
render() {
return (
<div className="App">
<Layout
adminMenu={this.state.adminMenu}
customerMenu={this.state.customerMenu}
name={this.state.name}
position={this.state.position}
/>
</div>
);
}
}

The “middlemen” — Layout.js

//Layout.js
import React, { Component } from "react";
import Profile from "./Profile/Profile";
import Menu from "./Menu/Menu";
import "../styles.css";
class Layout extends Component {
render() {
console.log(this.props.customerMenu);
return (
<div className="main-container">
<div className="left-container">
<Menu customerMenu={this.props.customerMenu} adminMenu={this.props.adminMenu}
/>
</div>
<div className="right-container">
<Profile name={this.props.name} position={this.props.position} />
</div>
</div>
);
}
}
export default Layout;

Bottom Level — Menu.js

class Menu extends Component {   render() {
return (
<div>
<p>{this.props.adminMenu ? <a href="#">Admin</a> : null}</p>
<p>{this.props.customerMenu ? <a href="#">Customer</a> : null}</p>
</div>
);
}
}

Bottom level — Profile.js

class Profile extends Component {   render() {
return (
<div>
<p>Name: {this.props.name}</p>
<p>Position: {this.props.position}</p>
</div>
);
}
}

In the above example, it shows that if we pass props from App to Menu / Profile, we must go through Layout.

Since the introduce of Context API, we can bypass Layout, App can pass props to Menu / Profile directly

How to implement Context API in ReactJS?

First we have to create a fail which initialize the context and its default value

This value inside createContext({}) is just for a reference. Even we leave empty here, we can still include it later in the <Provider>.

//user-context.js
import React from "react";
const userContext = React.createContext({});export default userContext;

In App.js, where we initialize the state and the starting point of passing state, we have to import the context file we just create.

Then, in render() — where we used to pass the props, we have to wrap the tree which contains Menu and Profile — the destination which we wanted to passed the props to, in this case — <Layout/>, and wrap it with <UserContext.Provider>, its value properties contains those state we would like to pass or we can just pass the whole state

import UserContext from "./context/user-context";class App extends Component {state = {
adminMenu: false,
customerMenu: true,
name: "Fifi",
position: "Customer Service Operator",
testRender: 1,
message: "testing"
};
//to test the behavior of context API later
increaseCounter = (greeting) => {
this.setState((state) => {
return { testRender: state.testRender + 1 };
});
};
render() {
console.log("App render");
console.log("counter:", this.state.testRender);
return (
<div className="App">
<UserContext.Provider
//pass the whole state
value={this.state}
//or only pass some value in state
/*value={{
adminMenu: this.state.adminMenu,
customerMenu: this.state.customerMenu,
name: this.state.name,
position: this.state.position
}}*/
>
<Layout />
</UserContext.Provider>
</div>
);
}
}

In Profile, another destination which we wanted to passed the props to. Import the context file we created at first step. This time we have to wrap the JSX which is referring context variable with <UserContext.Consumer>. And inside, before we can referring the context variable, we have to declare an arrow function here, with context as parameter, then we can wrap the JSX in return and referring the context variable there

import React, { Component, PureComponent } from "react";
import UserContext from "../../context/user-context";
class Menu extends Component {
render() {
console.log("Menu render");
return (
<UserContext.Consumer>
{(context) => (
<div>
<p>{context.adminMenu ? <a href="#">Admin</a> : null}</p>
<p>{context.customerMenu ? <a href="#">Customer</a> : null}</p>
</div>
)}
</UserContext.Consumer>
);
}
}
export default Menu;

In Profile, another destination which we wanted to passed the props to. Import the context file we created at first step.
This time we would try another syntax (require 16.6 or above). Inside the Profile class, we have to declare a static variable — static contextType (reserved word by React) and assign to UserContext (the context file we just import)

import UserContext from "../../context/user-context";class Profile extends Component {
static contextType = UserContext;

render() {
console.log("Profile render");
return (
<div>
<p>Name: {this.context.name}</p>
<p>Position: {this.context.position}</p>
</div>
);
}
}

What’s the difference between <Consumer> and contextType?

  1. <Consumer> available in React 16.3, contextType required React 16.6 above
  2. <Consumer> allows to consume multi context at the same time while contextType only allows to consume single context
  3. <Consumer> can only use with JSX while contextType can use in everywhere in the class
  4. contextType always trigger render() alongs with Parent once you declare static contextType = UserContext no matter you apply shouldComponentUpdate() or PureComponent
    On the other side, <Consumer> got the similar behavior, but if you apply shouldComponentUpdate() or PureComponent then it would stop trigger render() but it would still keep “re-render” or update anything inside <Consumer></Consumer>

Let’s try the working example

--

--