React just got ugly - React 16.4 Update

in #utopian-io6 years ago

Repository

https://github.com/facebook/react

Introduction

This post will talk about the recent changes in React 16.4 and the bugs they might introduce in your code.

Post Body

There was a recent update to React, 16.4, that changed how getDerivedStateFromProps works.
The main difference is that it's now being called even on state changes instead of, as the name suggests, just on prop changes - which is the way it worked in 16.3.

If you don't know about getDerivedStateFromProps, it's a static lifecycle method introduced in React 16.3 to prepare for async rendering.

In 16.3 it was proposed as an alternative for componentWillReceiveProps, which is deprecated and will be removed in React 17.

getDerivedStateFromProps is being added as a safer alternative to the legacy componentWillReceiveProps. - React 16.3

In my opinion, the removal of componentWillReceiveProps made it significantly more verbose to write components that have both local state, and also derive part of their state from props.

A fully controlled or fully uncontrolled component won't have the problems that we'll see now. So you should stick to these whenever possible, but in contrast to popular belief, we're often faced with components that are neither fully controlled nor uncontrolled.

Let's consider the following example. You have a Page component that renders the text of the current page and lets you edit it.
So far, Page can be uncontrolled - we keep the text in state and only update it when a button is pressed on the page, triggering an update to the parent component.

Now, let's add pagination: The parent component has a button that allows you to go to the next page, which will re-render your Page component with a new text prop.
This should now discard the local state in the Page component and render the text of the new page instead.

Here's a codesandbox of the app:

Demo

The App:

class App extends React.Component {
  state = {
    pages: ["Hello from Page 0", "Hello from Page 1", "Hello from Page 2"],
    currentPage: 0
  };

  onNextPage = () => {
    this.setState({
      currentPage: (this.state.currentPage + 1) % this.state.pages.length
    });
  };

  onUpdate = value => {
    const { pages, currentPage } = this.state;
    this.setState(
      {
        pages: [
          ...pages.slice(0, currentPage),
          value,
          ...pages.slice(currentPage + 1)
        ]
      }
    );
  };

  render() {
    const currentPageText = this.state.pages[this.state.currentPage];
    return (
      <div style={styles}>
        <Page value={currentPageText} onUpdate={this.onUpdate} />
        <button onClick={this.onNextPage}>Next Page</button>
      </div>
    );
  }
}

And here's the first try to implement the Page component:

import React from "react";

export default class Page extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: props.value,
    };
  }

  componentWillReceiveProps(nextProps) {
    // if new value props was received, overwrite state
    // happens f.i. when changing pages
      this.setState({
        value: nextProps.value
      });
  }

  // ALTERNATIVE: using getDerivedStateFromProps
  static getDerivedStateFromProps(props, state) {
      return {
        value: props.value
      };
  }

  onChange = event => {
    this.setState({
      value: event.target.value
    });
  };

  onSave = () => {
    this.props.onUpdate(this.state.value);
  };

  render() {
    return (
      <div
        style={{
          display: "flex",
          alignItems: "center",
          justifyContent: "center"
        }}
      >
        <textarea value={this.state.value} onChange={this.onChange} />
        <button onClick={this.onSave}>Save</button>
      </div>
    );
  }
}

The bug

The important thing to point out here is the use of componentWillReceiveProps or getDerivedStateFromProps to update the local text state when the page changed.

But right now, the Page component has a bug (even though it's not noticeable in the way it is used right now). We reset the state on every re-render. This is because of how componentWillReceiveProps / getStateDerivedFromProps worked:

componentWillReceiveProps: Note that if a parent component causes your component to re-render, this method will be called even if props have not changed. Make sure to compare the current and next values if you only want to handle changes. React Documentation

Now here's the fun part: Having this (buggy?) code that worked perfectly fine in our example app, doesn't work anymore in React 16.4 when you're using getDerivedStateFromProps:
The onChange on the textarea triggers a setState which itself triggers a getDerivedStateFromProps which sets the state again to the old text from props. Which means you cannot write in the textarea anymore.

Wait ... What?

The main problem with this code is that it was not resilient to re-renders. Because of this seemingly breaking change, there's a huge GitHub issue if this behavior in React 16.4 shouldn't be considered a breaking change which would require a major version bump.

The solution

The point I'm trying to make is a different one, however. It gets clear when we look at the suggested solution which fixes this bug:
As stated above, we need to compare the current and next values of props before setting the state.

This was easy for componentWillReceiveProps:

componentWillReceiveProps(nextProps) {
    // if new value props was received, overwrite state
    // happens f.i. when changing pages
    if (nextProps.value !== this.props.value) {
      this.setState({
        value: nextProps.value
      });
    }
  }

But not so easy with static getDerivedStateFromProps: It's static and we only receive (props, state) as arguments. To compare props with prevProps we have to save prevProps in state to be able to access it.

  constructor(props) {
    super(props);
    this.state = {
      prevProps: props,
      value: props.value,
    };
  }

  static getDerivedStateFromProps(props, state) {
    // comment this "if" and see the component break
    if (props.value !== state.prevProps.value) {
      return {
        prevProps: props,
        value: props.value
      };
    }
  }

That's ugly

Now take a step back. Think about the simplicity of the app and what we're trying to achieve: A text field. And then look at the solution code again.
There's clearly something wrong with React when this is the recommended way to handle such a fundamental use-case in React 16.4. To me, it feels hacky having to save the previous props in the state. There should be an easier way to do that. (You could also (ab)use the key attribute and do a full remount of the Page component avoiding getDerivedStateFromProps. This is described in another article, or this one. But that doesn't feel polished either.)

I sincerely hope that the React team's initiative to push async rendering doesn't further come at the cost of React's usability in everyday scenarios like the one above.
React 16.4 feels like a step backward after so many great and useful features in React 16.3.
With the removal of componentWillReceiveProps there is no simple way to just listen to props changes anymore or access previous props.


Originally published at https://cmichel.io

Resources

Sort:  

Hey, thanks for this great read. There is just one thing to mention. The contribution templates are there to give you a help for the structure but it is not 100% required to follow. What I mean is that you include Introduction and Post body sections which, in my opinion, lowers the appeal of this post.

If you want to include the introduction, we would be happier to see more than one sentence. Similarly to the section titles to correspond to the content and to be consistent with the headings levels. These are only minor things, though.

Your contribution has been evaluated according to Utopian policies and guidelines, as well as a predefined set of questions pertaining to the category.

To view those questions and the relevant answers related to your post, click here.


Need help? Write a ticket on https://support.utopian.io/.
Chat with us on Discord.
[utopian-moderator]

Thanks, I thought I had to stick to the templates. I agree, it takes the post out of flow. But now I know better for the next contribution :)

Hey @cmichel
Thanks for contributing on Utopian.
We’re already looking forward to your next contribution!

Contributing on Utopian
Learn how to contribute on our website or by watching this tutorial on Youtube.

Want to chat? Join us on Discord https://discord.gg/h52nFrV.

Vote for Utopian Witness!

Coin Marketplace

STEEM 0.17
TRX 0.15
JST 0.028
BTC 62205.55
ETH 2397.85
USDT 1.00
SBD 2.50