React-addons-perf
Copy import Perf from 'react-addons-perf';
Perf.start()
// App
Perf.stop()
The Perf methods can be used to take performance measurements.
Perf.printInclusive()
prints overall time taken.
Perf.printExclusive()
prints time excluding mounting time
Perf.printWasted()
prints time wasted on components that didn't actually render anything.
Perf.printOperations()
prints all DOM manipulations
Perf.getLastMeasurements()
prints the measurement from the last Perf session.
Avoid Reconciliation
shouldComponentUpdate()
Copy shouldComponentUpdate(nextProps, nextState) {
return this.props.color !== nextProps.color;
}
Here we use a shallow comparison to determine if the props of the component have changed. If so, the component should update.
React.PureComponent
This is a React component that implements shouldComponentUpdate()
and only diffs and updates when it returns true
. Any child of PureComponent
must also be a PureComponent
.
Copy class CounterButton extends React.PureComponent {
constructor(props) {
super(props);
this.state = {count: 1};
}
render() {
return (
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
);
}
}
Other hooks
useReducer
This is an alternative to useState. Accepts a reducer of type (state, action) => newState
, and returns the current state paired with a dispatch
method.
Cases to use useReducer over useState:
Complex state logic that involves multiple sub-values
The next state depends on the previous one
Copy const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
useCallback
This returns a memoized callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g. shouldComponentUpdate
).
Copy const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
Not Mutating Data
Code examples
Consider this example:
Copy class ListOfWords extends React.PureComponent {
render() {
return <div>{this.props.words.join(',')}</div>;
}
}
class WordAdder extends React.Component {
constructor(props) {
super(props);
this.state = {
words: ['hello']
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// This section is bad style and causes a bug
const words = this.state.words;
words.push('world');
this.setState({words: words});
}
render() {
return (
<div>
<button onClick={this.handleClick} />
<ListOfWords words={this.state.words} />
</div>
);
}
}
To solve this problem, we should avoid mutating values that we are using as props or state.
One solution using concat
can be:
Copy handleClick() {
this.setState(state => ({
words: state.words.concat(['world'])
}));
}
Or we can use the spread operator in ES6:
Copy handleClick() {
this.setState(state => ({
words: [...state.words, 'world'],
}));
};
For objects, we can also write code to avoid mutating objects. This is the way in which we mutate an object:
Copy function updateColorMap(colormap) {
colormap.right = 'blue';
}
However, we can avoid mutating the original object using Object.assign
in ES6:
Copy function updateColorMap(colormap) {
return Object.assign({}, colormap, {right: 'blue'});
}
Or, we can use the spread operator for objects from ES2018:
Copy function updateColorMap(colormap) {
return {...colormap, right: 'blue'};
}
Immer
This is the basic syntax using Immer to avoid mutating data:
Copy import produce from "immer"
const nextState = produce(baseState, draft => {
draft[1].done = true
draft.push({title: "Tweet about it"})
})
useState + Immer
Copy import React, { useCallback, useState } from "react";
import produce from "immer";
const TodoList = () => {
const [todos, setTodos] = useState([
{
id: "React",
title: "Learn React",
done: true
},
{
id: "Immer",
title: "Try Immer",
done: false
}
]);
const handleToggle = useCallback((id) => {
setTodos(
produce((draft) => {
const todo = draft.find((todo) => todo.id === id);
todo.done = !todo.done;
})
);
}, []);
const handleAdd = useCallback(() => {
setTodos(
produce((draft) => {
draft.push({
id: "todo_" + Math.random(),
title: "A new todo",
done: false
});
})
);
}, []);
return (<div>{*/ See CodeSandbox */}</div>)
}
useImmer
Copy import React, { useCallback } from "react";
import { useImmer } from "use-immer";
const TodoList = () => {
const [todos, setTodos] = useImmer([
{
id: "React",
title: "Learn React",
done: true
},
{
id: "Immer",
title: "Try Immer",
done: false
}
]);
const handleToggle = useCallback((id) => {
setTodos((draft) => {
const todo = draft.find((todo) => todo.id === id);
todo.done = !todo.done;
});
}, []);
const handleAdd = useCallback(() => {
setTodos((draft) => {
draft.push({
id: "todo_" + Math.random(),
title: "A new todo",
done: false
});
});
}, []);
// SeeCodeSandbox
useReducer and Redux + Immer
Similar to useState
, Immer also combines with useReducer
. And we have useImmerReducer
method from the use-immer package.
For Redux + Immer, refer to this doc of Redux Toolkit:
Other readings on immutable data
A few points to note from this article:
Avoid inline function definition in the render
function
Avoid using index as key for map
Avoid using props in initial states
Avoid spreading props on DOM elements
Avoid async initialization in componentWillMount()