受控表单元素和非受控表单元素

在html中,表单中可以使用入input、select等类型的数据输入控件,这些控件会自己保持自己的状态,,并根据用户输入更新状态。

在react中,数据通常保存在组件的state中,并且通过对应的set方法设置。

通过React状态控制输入的表单元素是受控的。
相反,由组件自己管理自己的状态是非受控的。

受控的表单元素

下面的表单元素,自己维护了自己的状态,name的值,当点击提交的时候,会将数据提交给web服务器。

<form action="" method="get" class="form-example">
  <div class="form-example">
    <label for="name">Enter your name: </label>
    <input type="text" name="name" id="name" required />
  </div>
  <div class="form-example">
    <input type="submit" value="Subscribe!" />
  </div>
</form>

类似的,在react中,表单控件也会管理好自己的数据,你可以通过ref来收集数据,最后在需要使用数据(比如提交)的时候,从实例变量上获取到数据。
非受控可以在UI交互逻辑简单,在简单的表单数据收集提交的场景被应用。

class Form extends Component {
  handleSubmitClick = () => {
    const name = this._name.value;
    // do something with `name`
  };

  render() {
    return (
      <div>
        <input type="text" ref={(input) => (this._name = input)} />
        <button onClick={this.handleSubmitClick}>Sign up</button>
      </div>
    );
  }
}

接着,让他变成受控的。
首先需要在组件中声明一个状态,用于维护输入控件的当前值。
然后在输入元素上需要有一个属性(input元素对应的是value)用来接受他的当前值,以及一个回调函数(onchange)用来修改状态。

当用户在输入一个字符时,handleNameChange被调用,将输入元素的当前值设置到状态中,状态改变后,组件使用新的value值重新渲染。
因此,react状态中始终维护了表单的当前值。

受控的好处是:

  • 能即时的收集到所有的表单状态
  • 能自定义的处理输入校验
  • 可以方便的将数据同步到UI上。
class Form extends Component {
  constructor() {
    super();
    this.state = {
      name: "",
    };
  }

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

  render() {
    return (
      <div>
        <input type="text" value={this.state.name} onChange={this.handleNameChange} />
      </div>
    );
  }
}

受控组件和非受控组件

受控组件和非受控组件的区别

非受控组件是指组件状态封闭在组件内,不受外部环境的影响。
受控组件指可以接受外部环境传入的value值,改变组件内部状态的组件。
对于组件库的一些组件,比如Input组件,需要做到既支持受控模式,又支持非受控模式。

非受控组件

对于下面的例子,这个Input组件中只有内部有一个状态,没有任何属性接受外部的状态,它是一个非受控的组件。

import React, { useState } from "react";

export const Input: React.FC = () => {
  const [value, setValue] = useState("");

  return (
    <>
      <input
        value={value}
        onChange={(e) => {
          const val = e.target.value;
          setValue(val);
        }}
      ></input>
      当前值为: {value}
    </>
  );
};

export default function App() {
  return (
    <div className="App">
      <Input></Input>
    </div>
  );
}

支持受控模式和非受控模式

简单实现

接着,给组件添加一个value属性和onChange属性,让组件同时支持受控模式和非受控模式。
设计思路:
在Input组件中始终维护一个状态,无论是否处于受控模式,它都直接使用自己的状态。当它处于受控模式时,让内部的状态手动和外部状态保持一致。

import React, { useEffect, useState } from "react";
import { isUndefined } from "util";

interface CustomInputComProps {
  value?: string;
  onChange?: (value: string) => void;
}

export const Input: React.FC<CustomInputComProps> = (props) => {
  const [value, setValue] = useState(props.value);
  const isControlled = !isUndefined(props.value);

  useEffect(() => {
    // 如果是受控模式,让内部的状态和外部的状态保持一致
    if (isControlled) {
      setValue(props.value);
    }
  }, [props.value]);

  return (
    <>
      <input
        value={value}
        onChange={(e) => {
          const val = e.target.value;
          // 如果是非受控模式,自己管理自己的状态
          if (!isControlled) setValue(val);
          props.onChange?.(val);
        }}
      ></input>
      当前值为: {value}
    </>
  );
};

export default function App() {
  const [name, setName] = useState("");
  return (
    <div className="App">
      <Input
        value={name}
        onChange={(value) => {
          setName(value);
        }}
      ></Input>
    </div>
  );
}

存在的问题:

  1. 由于通过setState更新子组件的状态,所以子组件状态的更新比父组件晚一个渲染周期。
  2. 受控模式下,内部状态在useEffect中通过setState来进行状态同步,因此会产生额外的一次渲染,一般来说多一次的渲染带来的性能问题可以忽略不计,但对于复杂的组件,还是可以继续优化。

改进1

解决方式:

  1. 对于子组件状态的更新比父组件晚一个渲染周期的问题:子组价和父组件的状态同步实际无需时刻一致,只需要判断,在组件处于受控模式下,直接使用来自外部的状态即可。
import React, { useEffect, useState } from "react";
import { isUndefined } from "util";

interface CustomInputComProps {
  value?: string;
  onChange?: (value: string) => void;
}

export const Input: React.FC<CustomInputComProps> = (props) => {
  const [value, setValue] = useState(props.value);
  const isControlled = !isUndefined(props.value);

  useEffect(() => {
    // 如果是受控模式,让内部的状态和外部的状态保持一致
    if (isControlled) {
      setValue(props.value); // 
    }
  }, [props.value]);

  const finalValue = isControlled ? props.value : value;

  return (
    <>
      <input
        value={finalValue}
        onChange={(e) => {
          const val = e.target.value;
          // 如果是非受控模式,自己管理自己的状态
          if (!isControlled) setValue(val);
          props.onChange?.(val);
        }}
      ></input>
      当前值为: {value}
    </>
  );
};

export default function App() {
  const [name, setName] = useState("");
  return (
    <div className="App">
      <Input
        value={name}
        onChange={(value) => {
          setName(value);
        }}
      ></Input>
    </div>
  );
}

改进1的抽象

import React, { useEffect, useState } from "react";
import { isUndefined } from "util";
import { useUpdateEffect } from "ahooks";

type Options<T> = {
  value?: T;
  defaultValue?: T;
};

export function usePropsValue<T>(
  options: Options<T>
): [T, React.Dispatch<React.SetStateAction<T>>] {
  const isControlled = !isUndefined(options.value);
  const [state, setState] = useState<T>(
    isControlled ? options.value : options.defaultValue
  );

  useUpdateEffect(() => {
    // 外部value改变时
    // 如果移除了value,则更新内部上
    // 否则,不同步到内部状态上,(如果同步到内部状态上,会引起额外的更新)
    if (options.value === undefined) {
      setState(options.value);
    }
  }, [options.value]);

  const finalValue = isControlled ? options.value : state;

  return [finalValue, setState];
}

改进2

对于setState产生额外的更新问题,可以通过ref存数据+手动更新来处理。

import React, { useRef } from "react";
import { isUndefined } from "util";
import { useUpdate } from "ahooks";

type Options<T> = {
  value?: T;
  defaultValue?: T;
};

export function usePropsValue<T>(options: Options<T>) {
  const isControlled = !isUndefined(options.value);
  const update = useUpdate();
  const stateRef = useRef(isControlled ? options.value : options.defaultValue);
  if (isControlled) {
    // 受控模式下,同步外部状态到内部状态
    stateRef.current = options.value;
  }
  const setState = (nextValue: T) => {
    if (nextValue === stateRef.current) return; // 避免不必要的update
    stateRef.current = nextValue;
    update(); // 更新组件
  };

  return [stateRef.current, setState] as const;
}
import React, { useState } from "react";
import { isUndefined } from "util";
import { usePropsValue } from "./usePropsValue";

interface CustomInputComProps {
  value?: string;
  onChange?: (value: string) => void;
}

export const Input: React.FC<CustomInputComProps> = (props) => {
  const isControlled = !isUndefined(props.value);

  const [value, setValue] = usePropsValue(props);

  return (
    <>
      <input
        value={value}
        onChange={(e) => {
          const val = e.target.value;
          // 如果是非受控模式,自己管理自己的状态
          if (!isControlled) {
            setValue(val);
          }
          props.onChange?.(val);
        }}
      ></input>
      当前值为: {value}
    </>
  );
};

export default function App() {
  const [name, setName] = useState("");
  return (
    <div className="App">
      <Input
        value={name}
        onChange={(value) => {
          console.log("onChange触发了", value);
          setName(value);
        }}
      ></Input>
    </div>
  );
}

参考

Controlled and uncontrolled form inputs in React don't have to be complicated
React 系列十:受控组件和非受控组件
antd mobile 作者教你写 React 受控组件和非受控组件


清风
1 声望0 粉丝

« 上一篇
sql 常用语句
下一篇 »
[rollup]