Lifecycle, state, getDerivedStateFromProps and Hooks

Thomas Rubattel
The Startup
Published in
5 min readAug 26, 2019
Credits : Wojciech Maj

Introduction

React 16.3 introduced a new lifecycle function called getDerivedStateFromProps.

This new lifecycle function is meant as replacement to componentWillReceiveProps which is unsafe for async rendering when misused. componentWillReceiveProps will no longer be supported from React 17, unlike its counterpart UNSAFE_componentWillReceiveProps.

Aside

In this tutorial we’ll use the so-called class properties which is quite used in the React community (e.g. Facebook) and which will save us some bits of code. You may use the Babel plugin called class-properties on this purpose.

For the sake of simplicity we did not make use of propTypes and we put all our React components together. Of course this is only to save some lines of code for the sake of this tutorial.

What is a lifecycle ?

A React component is a reusable piece of code receiving props to communicate (interface) with the outside world, may have states for the internal logic of the component, and producing a UI element.

A React component go through discrete steps called the lifecycle.

1. An instance of a component is created.
2. The component’s UI is computed (rendering) and mounted into the DOM.
3. Then an event occurs (e.g. user keystroke) and the already existing component will get new props. The component will be updated and thus will re-render.
4. The component is unmounted (e.g. routing or conditional rendering like isLoading && <Spinner /> upon state update).

Use case

We are going to discuss in this article about a very common use case : a React component will get new props following an UI event and the state of this component has to be updated accordingly.

In our concrete example we have three React components. One component having a controlled input called IframeInteractive which uses the component IframeWithSpinner having the state isLoading which needs to be sync with new props. IframeWithSpinner in turn uses another component called Iframe.

Lifecycle and state

1. Event occurs (user is typing)
2. IframeWithSpinner creation (url is passed as props)
3. Iframe creation (url and onLoad are passed as props)
4. IframeWithSpinnerand Iframe rendering
5. Event occurs (end of loading)
6. onLoad passed as props is called
7. IframeWithSpinner state update
8. IframeWithSpinner and Iframe component update
9. IframeWithSpinner and Iframe rer-endering

Illustration

Update state on new props with getDerivedStateFromProps

Before React 16.3

Before React 16.3, things were a bit more straightforward :

class IframeWithSpinner extends React.Component {
state = {
isLoading: true,
};

handleOnLoad = (e) => this.setState({ isLoading: false });

componentWillReceiveProps(nextProps){
if(this.props.url !== nextProps.url){
this.setState({isLoading: true});
}
}

render() {
const [{ url }, { isLoading }] = [this.props, this.state];
return (
<div>
{isLoading && <img src={spinner} alt="spinner" />}
<Iframe url={url} onLoad={this.handleOnLoad} />
</div>
);
}
}

From React 16.3

getDerivedStateFromProps is a static function and like the render function getDerivedStateFromProps is meant to be pure. This design choice was made for preparing the migration to async rendering.

Within getDerivedStateFromProps in IframeWithSpinner we cannot access to its current url props, since this lifecycle function has no access to the this context. So we set an additional state for keeping track of it, that we called prevUrl.

import React from "react";
import isURL from "validator/lib/isURL";
import spinner from "./ring-loader.gif";

class Iframe extends React.Component {
handleOnLoad = (e) => this.props.onLoad ? this.props.onLoad(e) : null;

render() {
return <iframe src={this.props.url} onLoad={this.handleOnLoad} />;
}
}

class IframeWithSpinner extends React.Component {
state = {
isLoading: true,
prevUrl: this.props.url
};

static getDerivedStateFromProps(props, state) {
if (props.url !== state.prevUrl) {
return {
isLoading: true,
prevUrl: props.url
};
}

return null;
}

handleOnLoad = e => this.setState({ isLoading: false });

render() {
const [{ url }, { isLoading }] = [this.props, this.state];

return (
<div>
{isLoading && <img src={spinner} alt="spinner" />}
<Iframe url={url} onLoad={this.handleOnLoad} />
</div>
);
}
}

export default class IframeInteractive extends React.Component {
state = {
url: ""
};

handleOnChange = e => this.setState({ url: e.target.value });

render() {
const { url } = this.state;
return (
<div>
<input value={url} onChange={this.handleOnChange} />
{isURL(url) && <IframeWithSpinner url={url} />}
</div>
);
}
}

With React Hooks

React 16.8 introduced React Hooks. There are two ways of solving this derived state issue with React Hooks.

Firstly, by explicitly synchronising new props with the component’s state by using useState :

const IframeWithSpinner = ({ url }) => {
const [prevUrl, setPrevUrl] = useState(url);
const [isLoading, setIsLoading] = useState(true);

const handleOnLoad = (e) => setIsLoading(false);

if (url !== prevUrl) {
setPrevUrl(url);
setIsLoading(true);
}

return (
<div>
{isLoading && <img src={spinner} alt="spinner" />}
<Iframe url={url} onLoad={handleOnLoad} />
</div>
);
}

Secondly, with useEffect which does the same as the example above with useState but implicitly.

const IframeWithSpinner = ({ url }) => {
const [isLoading, setIsLoading] = useState(true);

const handleOnLoad = (e) => setIsLoading(false);

useEffect(() => {
setIsLoading(true);
}, [url]);

return (
<div>
{isLoading && <img src={spinner} alt="spinner" />}
<Iframe url={url} onLoad={handleOnLoad} />
</div>
);
}

Better : use key

This technique forces the creation of a brand new component on new props. This means no component update, no state to update on component update, no additional state for keeping track of previous passed props and thus less complexity.

import React from "react";
import isURL from "validator/lib/isURL";
import spinner from "./ring-loader.gif";

class Iframe extends React.Component {
handleOnLoad = (e) => this.props.onLoad ? this.props.onLoad(e) : null;

render() {
return <iframe src={this.props.url} onLoad={this.handleOnLoad} />;
}
}

class IframeWithSpinner extends React.Component {
state = {
isLoading: true
};

handleOnLoad = e => this.setState({ isLoading: false });

render() {
const [{ url }, { isLoading }] = [this.props, this.state];

return (
<div>
{isLoading && <img src={spinner} alt="spinner" />}
<Iframe url={url} onLoad={this.handleOnLoad} />
</div>
);
}
}

export default class IframeInteractive extends React.Component {
state = {
url: ""
};

handleOnChange = e => this.setState({ url: e.target.value });

render() {
const { url } = this.state;
return (
<div>
<input value={url} onChange={this.handleOnChange} />
{isURL(url) && <IframeWithSpinner key={url} url={url} />}
</div>
);
}
}

Sum up

The next big feature of React will be async rendering which is made up of Concurrent mode and Suspense.

As part of the migration process, do write your components in a more functional programming way. React Hooks help in that regard. Make sure your render function is pure. Replace componentWillReceiveProps by getDerivedStateFromProps which also needs to be pure, or by React Hooks. If possible do not use getDerivedStateFromProps at all.

componentDidMount and componentDidUpdate are the only lifecycle functions where you might put side-effect in, for example Ajax call, local storage access, DOM access. componentDidUpdate should not call setState, except for updating an internal state based on a DOM element property (width, height, position).

States are, in general, very difficult to manage. Do favor stateless code by composing types. Feel free to have a look at an article I wrote on functional programming.

--

--