Lifecycle, state, getDerivedStateFromProps and Hooks
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
.
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. IframeWithSpinner
and 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
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.