1

I face a problem once update on React 16.4 where we have some breaking changes with getDerivedStateFromProps logic. Now it fires on each component update on both incoming and own component's props.

So, I've read the docs and manuals, but still can't figure out with cases where form input fields should be based on incoming props (controlled component) and, at the same time, be able to modify by the user own input?

I've also tried this post, but it just covers cases for a one-time update, not the manual input case: Why getDerivedStateFromProps is called after setState?

Here is my little code to reproduce:

import PropTypes from 'prop-types'
import React from 'react'

export class NameEditor extends React.Component {
  static propTypes = {
    currentLevel: PropTypes.number
  }

  static defaultProps = {
    currentLevel: 0
  }

  constructor(props) {
    super(props)

    this.state = {
      currentLevel: 0
    }
  }

  static getDerivedStateFromProps(nextProps) {
    return {
      currentLevel: nextProps.currentLevel
    }
  }

  _handleInputChange = e => {
    this.setState({
      currentLevel: e.target.value
    })
  }

  render() {
    const { currentLevel } = this.state

    return (
        <input
          placeholder={0}
          value={currentLevel}
          onChange={this._handleInputChange}
        />
    )
  }
}

export default NameEditor
Max Travis
  • 1,228
  • 4
  • 18
  • 41

3 Answers3

1

UPDATED:

Currently, _handleInputChange method will only modify the state of the child component, which will invoke the getDerivedStateFromProps.

The way that method works is, it get's invoked when every newProps or a setState call has occured.

Thus, the behavior is as follows:

  1. You change the value with your handler.
  2. getDerivedStateFromProps get's invoked, which will get the currentLevel value from the parent component, which is still not modified as we didn't do any changes there, therefore, it will overwrite the new value coming from invoking your handler, with the value that exist in the parent component, which wasn't modified.

To solve this: we will need a callback function coming from the parent component, which does the same work as the handleInputChange.

So:

  1. Add a handleCurrentLevelChange method to your parent component, which will have only one parameter e.target.value, it job is to modify the currentLevel at your parent state.
  2. Pass the handleCurrentLevelChange you created to your NameEditor the name you want, possibly the same name.
  3. Modify your child's handlr as follows:
  _handleInputChange = (e, cb) => {
    this.setState({
      currentLevel: e.target.value
    }, () => {
      cb && cb(e.target.value) //this makes the callback optional.
    });
  }
  1. Modify your onChange property to fit the new updates:
        <input
          placeholder={0}
          value={currentLevel}
          onChange={(e) => this._handleInputChange(e, handleCurrentLevelChange)}

The new behavior of the onChange property and handler will allow the changes to happen both at your child and your parent.

This should be solving the current problem.

Sultan H.
  • 2,908
  • 2
  • 11
  • 23
  • Interesting idea, but for not I'm tried to figure out how to deal on the component in isolation with such problem. – Max Travis Jul 23 '19 at 08:00
  • Thank you! I understood you from the very beginning, but the point was not brought logic outside the original Component (not make any improvements on parent component). But it's a good idea anyway! – Max Travis Jul 23 '19 at 08:54
  • Great, Sorry I didn't catch what you wanted from the start, I am glad you have found what you actually need. good luck @MaxTravis – Sultan H. Jul 23 '19 at 15:29
1

SOLUTION #1 (with key and remount):

You probably need to make your current component remount on each outer props update by providing it with a key, based on your incoming prop: currentLevel. It would looks like:

class Wrapper ... {
...

  render() {
    const { currentLevel } = this.props;

    return (
     <NameEditor key={currentLevel} {...currentLevel} />
    )
  }
}

export default Wrapper

...and make some extra changes on your component to block derived props replacing by telling it - is it a first time render or not (because we plan to control its state from inside only and from outer only by remount, when it really so):

import PropTypes from 'prop-types'
import React from 'react'

export class NameEditor extends React.Component {
  static propTypes = {
    currentLevel: PropTypes.number
  }

  static defaultProps = {
    currentLevel: 0
  }

  constructor(props) {
    super(props)

    this.state = {
      currentLevel: 0,
      isFirstRender: false
    }
  }

  static getDerivedStateFromProps(nextProps, prevProps) {
    if (!prevProsp.isFirstRender) {
      return {
        currentLevel: nextProps.currentLevel,
        isFirstRender: true
      };
    }

    return null;
  }

  _handleInputChange = e => {
    this.setState({
      currentLevel: e.target.value
    })
  }

  render() {
    const { currentLevel } = this.state

    return (
        <input
          placeholder={0}
          value={currentLevel}
          onChange={this._handleInputChange}
        />
    )
  }
}

export default NameEditor

So, by that scenario you'll achieve chance to manipulate your component state by manually inputed value from form.

SOLUTION #2 (without remount by flag):

Try to set some flag to separate outer (getDerived...) and inner (Controlled Comp...) state updates on each rerender. For example by updateType:

import PropTypes from 'prop-types'
import React from 'react'

export class NameEditor extends React.Component {
  static propTypes = {
    currentLevel: PropTypes.number
  }

  static defaultProps = {
    currentLevel: 0
  }

  constructor(props) {
    super(props)

    this.state = {
      currentLevel: 0,
      updateType: 'props' // by default we expecting update by incoming props
    } 
  }

  static getDerivedStateFromProps(nextProps, prevProps) {
    if (!prevState.updateType || prevState.updateType === 'props') {
      return {
        updateType: 'props',
        currentLevel: nextProps.currentLevel,
        exp: nextProps.exp
      }
    }

    if (prevState.updateType === 'state') {
      return {
        updateType: '' // reset flag to allow update from incoming props
      }
    }

    return null
  }

  _handleInputChange = e => {
    this.setState({
      currentLevel: e.target.value
    })
  }

  render() {
    const { currentLevel } = this.state

    return (
        <input
          placeholder={0}
          value={currentLevel}
          onChange={this._handleInputChange}
        />
    )
  }
}

export default NameEditor

P.S. It's probably an anti-pattern (hope Dan will never see this), but I can't find a better solution in my head now.

SOLUTIONS #3:

See Sultan H. post under this one, about controlled logic with explicit callback from wrapper component.

Sviat Kuzhelev
  • 1,758
  • 10
  • 28
0

because after set state React invoke render, but before render you always invoke getDerivedStateFromProps method.

setState schedules an update to a component’s state object. When state changes, the component responds by re-rendering

getDerivedStateFromProps is invoked right before calling the render method, both on the initial mount and on subsequent updates. It should return an object to update the state, or null to update nothing.

https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops

Vadim Hulevich
  • 1,803
  • 8
  • 17