You should be able to discuss the following topics after watching this video:
onClick as an arrow function, bind, non-bind
setState as a function and via state destruct
Checking for prev value in componentDidUpdate
Defining state examples, in constructor, as a property
Updating state in component did-update consequences
Keeping previous property in state
This tutorial builds on the previous one. If you’ve been wanting to work with ScandiPWA you’ll know that it’s meant to be extended. This is done by creating new files in your project that will override the defaults.
Run yarn start to start the development set-up and let’s start by overriding the index.js file. Create a new index.js file in your src folder. The application should compile automatically after you’ve saved any changes.
This is what our src/index.js should contain:
import { Component } from'react';import ReactDOM from'react-dom';classButtonextendsComponent {render () {return ( <button>Click me!</button> ); }}ReactDOM.render(// this is the component's template <Button />,document.getElementById('root'));
The component should be passed as a render template and after this we should tell React where we expect this element to render.
If you go to localhost:3000 in your browser you’ll see that the element has rendered.
In React you need to provide a listener for the element when it’s initially rendered, with the listener in this case being <button onClick={ this.onButtonClick }>. You can read more about handling events in React here.
Component state
Let’s set the default state and change it dynamically. The following code should start counting the clicks from zero:
import { Component } from'react';import ReactDOM from'react-dom';classButtonextendsComponent { state = {// sets the default state clickCount:0 };onButtonClick= () => {// imports the default stateconst { clickCount } =this.state;// updates the new state this.setState({ clickCount: clickCount +1 })console.log('you clicked me!'); };render () {return ( <buttononClick={ this.onButtonClick }>Click me!</button> ); }}ReactDOM.render( <Button />,document.getElementById('root'));
We need to transform the simple method onButtonClick() to a function property in order to not get a TypeError: Cannot read property ‘state’ of undefined. This can be done by adding an arrow function to onButtonClick = () =>. You can read about passing fuctions to components in React here.
Components update tracking logic
Let’s see how we can update the component with a simple tracking feature:
const { clickCount } = this.state; lets us further on just use { clickCount } to refer to this.state.clickCount. These are called state hooks and you can read more about them here.
import { Component } from'react';import ReactDOM from'react-dom';classButtonextendsComponent {// proper state declaration// state = {// clickCount: 0 // };// alternative state declarationconstructor (props){super(props);this.state = { clickCount:0 }; }onButtonClick= () => {const { clickCount } =this.state;this.setState({ clickCount: clickCount +1 }); };render () {// imports the latest click count from stateconst { clickCount } =this.state; return ( <div> <span> You clicked me <b>{ clickCount }</b> </span> <buttononClick={ this.onButtonClick }>Click me!</button> </div> ); }}ReactDOM.render( <Button />,document.getElementById('root'));
An alternative way to set the default state would be the following, which is very similar to PHP:
classButtonextendsComponent {constructor (props){// reference to a parent class that we extendsuper(props);this.state = { clickCount:0 }; }
Let’s consider the component’s life cycle. We’ve created a state, but how can we trace what the component is/was doing?
First, let’s name our division <div id="abc">, add some console logs and find out when they’ll get triggered:
import { Component } from'react';import ReactDOM from'react-dom';classButtonextendsComponent {constructor (props){super(props);this.state = { clickCount:0 };// make any request here, besides DOM manipulationconsole.log('constructor',document.getElementById('abc')); }componentDidMount() {// implement any DOM manipulationconsole.log('mount',document.getElementById('abc')); }onButtonClick= () => {const { clickCount } =this.state;this.setState({ clickCount: clickCount +1 }); };render () {const { clickCount } =this.state; return ( <divid="abc"> <span> You clicked me <b>{ clickCount }</b> </span> <buttononClick={ this.onButtonClick }>Click me!</button> </div> ); }}ReactDOM.render( <Button />,document.getElementById('root'));
When we try this out in our browser we can see in the Console that the constructor returns null and mount returns <div id="abc"></div>.
This shows us that the component’s constructor is called before it is mounted. You can read more about the constructor here and componentDidMount here.
If you want to add a CSS variable or have access to a DOM node, you can do it from here:
componentDidMount() {// implement any DOM manipulationconsole.log('mount',document.getElementById('abc')); }
Next, we have the componentDidUpdate() method which allows us to see if something was updated in the component:
componentDidMount() {// implement any DOM manipulationconsole.log('mount',document.getElementById('abc')); }componentDidUpdate() {// triggered by state & props changeconsole.log('update'); }
After adding the update log you should be able to see update in the Console any time the button had been clicked.
Let’s add a new class. If ESlint is showing you a ‘max classes per file’ error, disable it for the sake of this tutorial. Note that when developing an actual project, try to stick to the one component per file rule.
You can disable ESlint rule warnings for the entire file by adding the /* eslint-disable */ at the top of it.
/* eslint-disable max-classes-per-file, @scandipwa/scandipwa-guidelines/only-one-class */import { Component } from'react';import ReactDOM from'react-dom';classButtonextendsComponent {constructor (props){super(props);this.state = { clickCount:0 };console.log('constructor',document.getElementById('abc')); }componentDidMount() {console.log('mount',document.getElementById('abc')); }componentDidUpdate() {// triggered by state & props changeconsole.log('update'); } onButtonClick= () => {const { clickCount } =this.state;this.setState({ clickCount: clickCount +1 }); };render () {const { clickCount } =this.state; return ( <divid="abc"> <span> You clicked me <b>{ clickCount }</b> </span> <buttononClick={ this.onButtonClick }>Click me!</button> </div> ); }}// the new classclassWrapperextendsComponent {// defines the state state = { clickCount:0 };onButtonClick= () => {const { clickCount } =this.state;this.setState({ clickCount: clickCount +1 }); };render() {return( <div> {/* calls the button component */} <Button /> {/* button with event listener */} <buttononClick={ this.onButtonClick }>Update wrapper</button> </div> ); }}ReactDOM.render(// renders the wrapper component instead <Wrapper />,document.getElementById('root'));
Let’s go to our browser and try out the changes. So, if you click on the 1st button, the update will get triggered in Console, but if you click the 2nd button, the update will also get triggered. Why is that? This happens because any updates in the top component will update the bottom component in a very inefficient way.
Going back to the browser we can see that the clicks in Button will trigger updates, but clicks in Wrapper will not. The shouldComponentUpdate() method might get tedious if you’re working with multiple components. This is where PureComponents come in.
PureComponents VS traditional Components
PureComponent performs a shallow comparison of the props and state any time props or state changes. PureComponent essentially is Component with built in shouldComponentUpdate method.Copy
Now, when trying this out in the browser, the update should also get triggered when clicking the ‘Update wrapper’ button.
Let’s go back to the Button class. In order to change up the componentDidUpdate method we need to either set the required prop or set the default value.
This is where ESLint would notify you that you shouldn’t use setState in componentDidUpdate. This is because setting the state here could lead to an infinite loop if the checkpoint before doing so would not be specific enough.
Read more about componentDidUpdate and infinite loops here.
Handling side-effects in getDerivedStateFromProps
We can bypass the componentDidUpdate issue by using getDerivedStateFromProps. Note that this method can’t access any values from the component.
classButtonextendsPureComponent {// use either propTypes or defaultPropsstatic propTypes = {// sets the required prop wrapperCount:PropTypes.number };static defaultProps = {// sets the default value wrapperCount:0 };constructor (props){super(props);this.state = { clickCount:0,// set a new default prevWrapperCount:0 }; static getDerivedStateFromProps(props, state) {// no access to current value is present// no access to `this`const { wrapperCount } = props;const { prevWrapperCount } = state;// you need to keep previous value in state// if wrapper count is not equal to previous valueif (wrapperCount !== prevWrapperCount) {return {// update click count to wrapper count clickCount: wrapperCount,// update previous value prevWrapperCount: wrapperCount }; }returnnull; }componentDidMount() {console.log('mount',document.getElementById('abc')); }...
You should put getDerivedStateFromProps before componentDidMount. This should work as previously with componentDidUpdate, except there is no possibility to enter an infinite loop.