2

Author: Frost

Proofreading: Kangaroo Cloud Data Stack Front-end Team Operations Team

The article contains the following

  • Controlled and Uncontrolled Components

    • uncontrolled components
    • Controlled Components
  • Controlled and uncontrolled component boundaries
  • anti-pattern
  • solution

foreword

In HTML, form elements ( <input> / <textarea> / <select> ), usually maintain state themselves and update them based on user input

<form>
  <label>
    名字:
    <input type="text" name="name" />
  </label>
  <input type="submit" value="提交" />
</form>

In this HTML, we can arbitrarily enter the value in the input. If we need to get the content entered by the current input, what should we do?

Controlled and Uncontrolled Components

uncontrolled component

Using uncontrolled components, instead of writing data processing functions for each state update, the form data is handed over to the DOM node for processing, and Ref can be used to get the data
In an uncontrolled component, you want to be able to give the form an initial value, but not control subsequent updates. A default value can be specified using defaultValue

class Form extends Component {
  handleSubmitClick = () => {
    const name = this._name.value;
    // do something with `name`
  }
  render() {
    return (
      <div>
        <input
                    type="text"
                    defaultValue="Bob"
                    ref={input => this._name = input}
                />
        <button onClick={this.handleSubmitClick}>Sign up</button>
      </div>
    );
  }
}

controlled component

In React, mutable state is usually stored in the component's state property and can only be updated via setState

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: 'shuangxu'};
  }
  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          名字:
          <input type="text" value={this.state.value}/>
        </label>
        <input type="submit" value="提交" />
      </form>
    );
  }
}

In the above code, the value attribute value is set in Input, so the displayed value is always this.state.value , which makes state the only data source.

const handleChange = (event) => {
    this.setState({ value: event.target.value })
}

<input type="text" value={this.state.value} onChange={this.handleChange}/>

If we write the handleChange method in the above example, then every keypress will execute the method and update the React state, so the value of the form will change with the user's input

React components control the operations of the form during user input and state is the only source of data. Form input elements whose values are controlled by React in this way are called controlled components

Controlled and uncontrolled component boundaries

uncontrolled components

The Input component only receives a default value of defaultValue . When calling the Input component, you only need to pass a defaultValue through props.

//组件
function Input({defaultValue}){
    return <input defaultValue={defaultValue} />  
}

//调用
function Demo(){
    return <Input defaultValue='shuangxu' />
}

Controlled Components

The display and change of the value need to be controlled by state and setState , the component controls the state internally, and implements its own onChange method

//组件
function Input() {
    const [value, setValue] = useState('shuangxu')
  return <input value={value} onChange={e=>setValue(e.target.value)} />;
}

//调用
function Demo() {
  return <Input />;
}

Is the Input component controlled or uncontrolled at this time? If we use the previous writing to change this component and its call

//组件
function Input({defaultValue}) {
    const [value, setValue] = useState(defaultValue)
  return <input value={value} onChange={e=>setValue(e.target.value)} />;
}

//调用
function Demo() {
  return <Input defaultValue='shuangxu' />;
}

The Input component at this point is itself a controlled component, which is driven by unique state data. But for Demo, we do not have a right to change the data of the Input component, so for the Demo component, the Input component is an uncontrolled component. (‼️It is an anti-pattern to call controlled components in the way of uncontrolled components)

How to modify the current Input and Demo component codes so that the Input component itself is also a controlled component, and it is also controlled for the Demo component?

function Input({value, onChange}){
    return <input value={value} onChange={onChange}
}

function Demo(){
    const [value, setValue] = useState('shuangxu')
    return <Input value={value} onChange={e => setValue(e.target.value)} />

Anti-pattern - calling controlled components in the same way as uncontrolled components

While controlled and uncontrolled are often used to point to form inputs, they can also be used to describe components with frequently updated data.
Through the boundary division of controlled and uncontrolled components in the previous section, we can simply classify them as:

  • If you use props to pass in data, there is a corresponding data processing method, and the component is considered controllable by the parent
  • The data is only stored in the state inside the component, the component is not controlled by the parent

⁉️ What is derived state

Simply put, if some data in a component's state comes from outside, that data is called derived state.

Most of the problems caused by using derived state are due to two reasons:

  • Copy props directly to state
  • Update state if props and state are inconsistent

    Copy prop directly to state

    ⁉️ Execution period of getDerivedStateFromProps and componentWillReceiveProps

  • Both lifecycles are executed when the parent re-renders, regardless of props changes
  • Therefore, it is not safe to directly copy props to the state in the two methods, which will cause the state to not be rendered correctly.

    class EmailInput extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        email: this.props.email   //初始值为props中email
      };
    }
    componentWillReceiveProps(nextProps) {
      this.setState({ email: nextProps.email });   //更新时,重新给state赋值
    }
    handleChange = (e) => {
      this.setState({ email: e.target.value });
    };
    render() {
      const { email } = this.state;
      return <input value={email} onChange={this.handleChange} />;
    }
    }

    view example

Set the initial value from the props to the Input, and it will modify the state when the Input is input. But if the parent component is re-rendered, the value of the input box Input will be lost and become the default value of props

Even if we compare nextProps.email!==this.state.email before reset still results in update

For the current small demo, you can use shouldComponentUpdate to compare whether the email in props has been modified and then decide whether it needs to be re-rendered. But for practical applications, this way of handling is not feasible, a component will receive multiple props, and changes to any one prop will cause re-rendering and incorrect state reset. Add in inline functions and object props, and it gets harder and harder to create a fully reliable shouldComponentUpdate . shouldComponentUpdate This lifecycle is more for performance optimization than for dealing with derived state.
So far, explain why cannot directly copy prop to state . Thinking about another question, what if you just use the email property in props to update the component?

Modify state after props change

Following the above example, only use props.email to update the component, which can prevent bugs caused by modifying the state

class EmailInput extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      email: this.props.email   //初始值为props中email
    };
  }
  componentWillReceiveProps(nextProps) {
        if(nextProps.email !== this.props.email){
        this.setState({ email: nextProps.email });   //email改变时,重新给state赋值
        }
  }
    //...
}

Through this transformation, the component will only reassign the state when props.email changes. Is there any problem with this transformation?

In the following scenarios, when switching between two accounts with the same email, this input box will not be reset, because the prop value from the parent component has not changed.
view example
This scene is constructed and may be oddly designed, but mistakes like this are common. There are two solutions to this anti-pattern. The point is, any data, we must ensure that there is only one data source, and avoid copying it directly.

solution

Fully controllable components

Remove the state from the EmailInput component, use props directly to get the value, and pass control of the controlled component to the parent component.

function EmailInput(props){
    return <input onChange={props.onChange} value={props.email}/>
}

If you want to save temporary values, the parent component needs to perform the save manually.

Uncontrolled component with key

Let the component store the temporary email state, the initial value of email is still accepted through prop, but the changed value has nothing to do with prop

function EmailInput(props){
    const [email, setEmail] = useState(props.email)
    return <input value={email} onChange={(e) => setEmail(e.target.value)}/>
}

In the previous example of switching accounts, in order to switch different values on different pages, you can use the React special property key . When the key changes, React will create a new component instead of simply updating the existing one ( for more ). We often use the key value when rendering dynamic lists, which can also be used here.

<EmailInput
    email={account.email}
    key={account.id}
/>

view example

Every time the id changes, EmailInput is recreated and its state is reset to the most recent email value.

Alternative

  1. Doing this with the key property resets the state of the entire component. You can observe the change of id at getDerivedStateFromProps and componentWillReceiveProps , which is troublesome but feasible
    view example

    class EmailInput extends Component {
      state = {
    email: this.props.email,
    prevId: this.props.id
      };
    
      componentWillReceiveProps(nextProps) {
    const { prevId } = this.state;
    if (nextProps.id !== prevId) {
      this.setState({
        email: nextProps.email,
        prevId: nextProps.id
      });
    }
      }
      // ...
    }
  2. Reset an uncontrolled component using an instance method
    There are just two ways, both in the case of having a unique identification value. If you also want to recreate the component when there is no suitable key value. The first is to generate a random or incremented value as the key value, the other is to use the example method to force a reset of the internal state
    The parent component uses ref to call this method, Click to see the example

    class EmailInput extends Component {
      state = {
    email: this.props.email
      };
    
      resetEmailForNewUser(newEmail) {
    this.setState({ email: newEmail });
      }
    
      // ...
    }

    How do we choose

In our business development, try to choose controlled components and reduce the use of derived state. Excessive use of componentWillReceiveProps may lead to incomplete props judgment, but repeated rendering of infinite loop problems.

In component library development, such as Ant Design, both controlled and uncontrolled calling methods are open to users, allowing users to choose the corresponding calling method independently. For example, in the Form component, we often use getFieldDecorator and initialValue to define form items, but we don't care about the intermediate input process at all, and get all the form values through getFieldsValue or validateFields at the final submission, which is an uncontrolled calling method. Or, when we have only one Input, we can directly bind the value and onChange events, which is called in a controlled manner.

Summarize

In this article, the concepts of uncontrolled and controlled components are first introduced. For a controlled component, the component controls the process of user input and state is the only source of data for the controlled component.

Then, the problem of component invocation is introduced. For the component caller, whether the component provider is a controlled component. For the caller, the boundary between controlled and uncontrolled components depends on whether the current component has control over changes to the subcomponent's value.

It then introduces the anti-pattern usage of invoking a controlled component as an uncontrolled component, along with related examples. Don't copy props directly to state, use controlled components instead. For uncontrolled components, when you want to reset the state when the prop changes, you can choose the following ways:

  • Suggestion: use the key property to reset all the initial state inside
  • Option 1: Change only some fields and observe the changes of special properties (properties with uniqueness)
  • Option 2: Use ref to call instance method

Finally, a summary of how to choose a controlled component or an uncontrolled component.

Reference link


袋鼠云数栈UED
277 声望34 粉丝

我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。