Search code examples
javascriptreactjsgetderivedstatefromprops

React 16.4 - manual form input fill along with its updates from getDerivedStateFromProps?


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

Solution

  • 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.