Skip to main content

React Optimisation

info

This page uses the API before DRAW v1.17, please take a look at Coded Visual Component for the correct API. However the concept explained here are still accurate.

Welcome to this tutorial, today we will try to understand how we can improve the performance of an app relying on React. At the end of this tutorial you should have some notion of React Hooks. You should be able to understand why it is important to let React handle the DOM rendering. You will see how to make a single ReactDOM.render() call when your Olympe properties and events change.

Before starting this tutorial, you should fulfil the following requirements:

All right, let's get do it !

Where we're at

This is the code you were left with at the end of the Advanced Code Tutorial:

import { UIBrick, registerBrick } from 'olympe';

//react imports
import React from 'react';
import ReactDOM from 'react-dom';
import { Bar } from 'react-chartjs-2';

//RxJS imports
import {startWith, map} from "rxjs/operators";
import {combineLatest} from 'rxjs';

export default class BarChart extends UIBrick {

/**
* This method runs when the brick is ready in the HTML DOM.
* @override
* @param {!Context} context
* @param {!Element} elementDom
*/
draw(context, elementDom) {
const titleProperty = context.getProperty('title');
const titleObservable = titleProperty.observe().pipe(startWith('My DEFAULT First Dataset'));

combineLatest([titleObservable]).subscribe(([_title]) => {
ReactDOM.render(<BarChart.component title={_title}/>, elementDom);
});
}
}

registerBrick('017ca7b8eb2e327c8eeb', BarChart);
BarChart.component = (props) => {
const labels = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
const data = {
labels: labels,
datasets: [{
label: props.title,
data: [65, 59, 80, 81, 56, 55, 40],
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(255, 159, 64, 0.2)',
'rgba(255, 205, 86, 0.2)',
'rgba(75, 192, 192, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(153, 102, 255, 0.2)',
'rgba(201, 203, 207, 0.2)'
],
borderColor: [
'rgb(255, 99, 132)',
'rgb(255, 159, 64)',
'rgb(255, 205, 86)',
'rgb(75, 192, 192)',
'rgb(54, 162, 235)',
'rgb(153, 102, 255)',
'rgb(201, 203, 207)'
],
borderWidth: 1
}]
};

return (
<Bar
data={data}
width={100}
height={50}
options={{ maintainAspectRatio: false }}
/>
)
}

First, we want to prevent ReactDOM.render() from being called multiple times while the title property is being updated. For this, we are going to use an advanced concept of React : the Hooks.

React Hooks

From React official documentation, Hooks are defined as such :

Hooks are functions that let you “hook into” React state and lifecycle features from function components.

Hooks-overview

Let's start with the simplest Hook there is, the state Hook :

useState()

From React official documentation (useState), we can find a simple example of useState hook usage :

import React, { useState } from 'react';

function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

So what's happening here ? every time you click on the button, the count state is being updated thanks to the setCount() function. React then detects that the count state variable has changed and triggers a new rendering of the Example component. This basically means that your render function gets called again with the new {count} value.

Using the same principle, we are going to transform the title property into a state property :

const [title, setTitle] = useState('');

And now the data label becomes :

 const data = {
labels: labels,
datasets: [{
label: title,
//rest stays the same

Now that we've taken care of the title, it's time that we connect it to the DRAW title property. Otherwise, the state will never change and the component will never get updated. As mentioned before, DRAW properties are RXJS observables that you can subscribe to whenever a change occurs. Notice how we subscribed to the property inside the draw() method before :

 draw(context, elementDom) {
const titleProperty = context.getProperty('title');
const titleObservable = titleProperty.observe().pipe(startWith('My DEFAULT First Dataset'));

combineLatest([titleObservable]).subscribe(([_title]) => {
ReactDOM.render(<BarChart.component title={_title}/>, elementDom);
});
}

Now we want to pass down the observable to ReactDOM.render(), so that React alone handles the title changes :

draw(context, elementDom) {
const titleProperty = context.getProperty('title');
const titleObservable = titleProperty.observe().pipe(startWith('My DEFAULT First Dataset'));
ReactDOM.render(<BarChart.component titleObservable={titleObservable}/>, elementDom);
}

All right, let's handle this titleObservable inside the React Component.

Infinite renders

The first thing you might want to try is to directly subscribe to the title property inside your component, like this :

import { UIBrick, registerBrick } from 'olympe';

//react imports
import React, {useState} from 'react';
import ReactDOM from 'react-dom';
import { Bar } from 'react-chartjs-2';

//rxjs imports
import {startWith, map} from "rxjs/operators";
import {combineLatest} from 'rxjs';

export default class BarChart extends UIBrick {

/**
* This method runs when the brick is ready in the HTML DOM.
* @override
* @param {!Context} context
* @param {!Element} elementDom
*/
draw(context, elementDom) {
const titleProperty = context.getProperty('title');
const titleObservable = titleProperty.observe().pipe(startWith('My DEFAULT First Dataset'));
console.log('Calling ReactDOM.render');
ReactDOM.render(<BarChart.component titleObservable={titleObservable}/>, elementDom);
}
}

registerBrick('017ca7b8eb2e327c8eeb', BarChart);
BarChart.component = (props) => {

const [title, setTitle] = useState('');

// *********** SUBSCRIPTION ************
combineLatest([props.titleObservable]).subscribe(([_title]) => {
setTitle(_title);
});
// ***************************************

const labels = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
const data = {
labels: labels,
datasets: [{
label: title,
data: [65, 59, 80, 81, 56, 55, 40],
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(255, 159, 64, 0.2)',
'rgba(255, 205, 86, 0.2)',
'rgba(75, 192, 192, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(153, 102, 255, 0.2)',
'rgba(201, 203, 207, 0.2)'
],
borderColor: [
'rgb(255, 99, 132)',
'rgb(255, 159, 64)',
'rgb(255, 205, 86)',
'rgb(75, 192, 192)',
'rgb(54, 162, 235)',
'rgb(153, 102, 255)',
'rgb(201, 203, 207)'
],
borderWidth: 1
}]
};

return (
<Bar
data={data}
width={100}
height={50}
options={{ maintainAspectRatio: false }}
/>
)
}

But when you run your code, you get an error :

If you check the logs in the browser console, you should see message Calling ReactDOM.render appearing only once. So, what's happening here ? Why are we receiving so many render calls ? Let's study a bit more the lifecycle of a React component to answer that question.

React Component Lifecycle methods

render()

Whenever we call ReactDOM.render() we need to pass a render method as first parameter. There are two ways of doing this :

  1. You can create a class object, extend the React.Component class, and override method render().
class Welcome extends React.Component {
/**
@override
**/
render() {
return <h1>Hi, {this.props.name}</h1>;
}
}
  1. You can define directly the render method as a function :
function Welcome(props) {
return <h1>Hi, {props.name}</h1>;
}

or as an arrow function :

const Welcome = (props) => {
return <h1>Hi, {props.name}</h1>;
}

In all cases, the call to ReactDOM.render() stays the same :

ReactDOM.render(<Welcome name={'John'}/>, elementDom);
React.Component Class

As you probably noticed, we defined React components as arrow functions throughout these tutorials, using the second solution. If you want to use the React component class approach, feel free to do so as both ways are completely valid.

So, as both solutions demonstrate, but in particular in the first case extending React.Component, we need to define a render method for the new React components. render is one of the many lifecycle method that a React component goes through. It is absolutely required that you override this lifecycle method in your React components. There are other lifecycle methods which you can optionally override. Let's look at one of them in particular :

componentDidMount()

componentDidMount() is a hook. We already saw the definition of a hook, but as we are all lazy here is a reminder:

Hooks are functions that let you “hook into” React state and lifecycle features from function components.

We already saw how hooks can influence the states of your application. Now, we see they can also influence the lifecycle of your components.

As mentioned before, overriding componentDidMount is optional.

componentDidMount(), as its name states, is called right after a React component has been mounted, i.e. after the first render() lifecycle method terminates.

It is used as such :

class App extends React.Component {
/**
* @override
**/
componentDidMount() {
// Runs after the first render() lifecycle
}

/**
* @override
**/
render() {
return <h1>Hello</h1>;
}
}

So what is the purpose of this hook ? Well, do you remember this troublesome bug ?

It turns out that the lifecycle of the render arrow function is not pure, meaning we try to re-render inside method render().

And here is the culprit :

//  *********** SUBSCRIPTION ************
combineLatest([props.titleObservable]).subscribe(([_title]) => {
setTitle(_title);
});
// ***************************************

The moment the subscription callback is resolved, with the title either being set or having a default value, we trigger a re-render by setting the title again. This is because React detects that one of it's state variable has changed (title) and therefore wants to update its virtual DOM by re-rendering it. This causes a classic React infinite re-render loop error.

What if we remove title default value ?

You could indeed try to remove .pipe(startWith('My DEFAULT First Dataset')); from the title observable. This way, the subscription is defined but not immediately resolved inside the render method lifecycle. Indeed, you will get your BarChart up and running but the moment you try to set the title property inside DRAW, the infinite loop would start all over again.

In conclusion, you must always keep the render() method pure, meaning you must avoid cases where a call to the render() method triggers another direct re-render (which is the case every time you update a state: a new render gets triggered by React).

So we need to find a way to set up a subscription once and keep the render() method pure. This can only be done with componentDidMount() hook. We would love to do something like this :

import { UIBrick, registerBrick } from 'olympe';

//react imports
import React, {useState} from 'react';
import ReactDOM from 'react-dom';
import { Bar } from 'react-chartjs-2';

//rxjs imports
import {startWith, map} from "rxjs/operators";
import {combineLatest} from 'rxjs';

export default class BarChart extends UIBrick {

/**
* This method runs when the brick is ready in the HTML DOM.
* @override
* @param {!Context} context
* @param {!Element} elementDom
*/
draw(context, elementDom) {
const titleProperty = context.getProperty('title');
const titleObservable = titleProperty.observe().pipe(startWith('My DEFAULT First Dataset'));
console.log('Calling ReactDOM.render');
ReactDOM.render(<BarChart.component titleObservable={titleObservable}/>, elementDom);
}
}

registerBrick('017ca7b8eb2e327c8eeb', BarChart);
BarChart.component = (props) => {
const [title, setTitle] = useState('');

/**
* Keep `render` lifecycle method pure by isolating the potential re-render calls
*/
componentDidMount() {
// *********** SUBSCRIPTION ************
combineLatest([props.titleObservable]).subscribe(([_title]) => {
setTitle(_title);
});
// ***************************************
}

const labels = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
const data = {
labels: labels,
datasets: [{
label: title,
data: [65, 59, 80, 81, 56, 55, 40],
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(255, 159, 64, 0.2)',
'rgba(255, 205, 86, 0.2)',
'rgba(75, 192, 192, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(153, 102, 255, 0.2)',
'rgba(201, 203, 207, 0.2)'
],
borderColor: [
'rgb(255, 99, 132)',
'rgb(255, 159, 64)',
'rgb(255, 205, 86)',
'rgb(75, 192, 192)',
'rgb(54, 162, 235)',
'rgb(153, 102, 255)',
'rgb(201, 203, 207)'
],
borderWidth: 1
}]
};

return (
<Bar
data={data}
width={100}
height={50}
options={{ maintainAspectRatio: false }}
/>
)
}

Don't execute your code yet, it won't work. But in theory, this is what the component lifecycle looks like now:

  1. ReactDOM.Render() gets called.
  2. First call to method render mounts the component with an empty title.
  3. componentDidMount() gets called, the subscription is defined once, and the title gets updated.
  4. Second call to method render updates the title with a default value. Even though there is a second call to method render, the update is optimised by React.
  5. Our component is ready.

Unfortunately for us, componentDidMount() works great for the class approach, but has no direct equivalent for the functional approach that we used throughout these tutorials. This is why your code won't compile right now. There is however an alternative hook that we can use called useEffect().

useEffect()

React official documentation (useEffect documentation) gives the following definition:

Side Effect: Data fetching, setting up a subscription, and manually changing the DOM in React components are all examples of side effects. Whether or not you’re used to calling these operations “side effects” (or just “effects”), you’ve likely performed them in your components before.

Since we set up a subscription, we therefore set up an effect, and we require the useEffect() hook. Here is a simple example:

import React, {useEffect, useState} from 'react';
const Welcome = (props) => {
const [name, setName] = useState('');

useEffect(() => {
// Runs after the first render() lifecycle
database.fetchData(data => {
//Triggers a new render
setName(data.name);
});
}, []);

return <h1>Hello, {name}</h1>;
}

Notice the second argument we pass to the useEffect() hook. This array represents the dependencies from props or states that the hook should listen to. Each time a dependency changes, the hook gets called again. For example:

useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}

ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // Only re-subscribe if props.friend.id changes

So what happens when we pass an empty array like in the example above ? Well, since no dependencies are declared, the hook is only called once after the first call to method render(). This is the same behaviour as for the componentDidMount() hook, which is exactly what we were looking for.

Our code now becomes:

import { UIBrick, registerBrick } from 'olympe';

//react imports
import React, {useEffect, useState} from 'react';
import ReactDOM from 'react-dom';
import { Bar } from 'react-chartjs-2';

//rxjs imports
import {startWith, map} from "rxjs/operators";
import {combineLatest} from 'rxjs';

export default class BarChart extends UIBrick {

/**
* This method runs when the brick is ready in the HTML DOM.
* @override
* @param {!Context} context
* @param {!Element} elementDom
*/
draw(context, elementDom) {
const titleProperty = context.getProperty('title');
const titleObservable = titleProperty.observe().pipe(startWith('My DEFAULT First Dataset'));
ReactDOM.render(<BarChart.component titleObservable={titleObservable}/>, elementDom);
}
}

registerBrick('017ca7b8eb2e327c8eeb', BarChart);
BarChart.component = (props) => {
const [title, setTitle] = useState('');

/**
* Keep `render` lifecycle method pure by isolating the potential re-render calls
*/
useEffect(() => {
// *********** SUBSCRIPTION ************
combineLatest([props.titleObservable]).subscribe(([_title]) => {
setTitle(_title);
});
// ***************************************
}, []);

const labels = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
const data = {
labels: labels,
datasets: [{
label: title,
data: [65, 59, 80, 81, 56, 55, 40],
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(255, 159, 64, 0.2)',
'rgba(255, 205, 86, 0.2)',
'rgba(75, 192, 192, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(153, 102, 255, 0.2)',
'rgba(201, 203, 207, 0.2)'
],
borderColor: [
'rgb(255, 99, 132)',
'rgb(255, 159, 64)',
'rgb(255, 205, 86)',
'rgb(75, 192, 192)',
'rgb(54, 162, 235)',
'rgb(153, 102, 255)',
'rgb(201, 203, 207)'
],
borderWidth: 1
}]
};

return (
<Bar
data={data}
width={100}
height={50}
options={{ maintainAspectRatio: false }}
/>
)
}

The subscription is defined only once and the ReactDOM.render() method gets called only once. We let React handle the updates and we should gain a lot of performance because React optimises those extra render calls. However, despite the better performances, the graph is still redrawn every time the title property changes, which is not ideal.

In the next tutorial we will see how we can push rendering performances even further.