kangyana / daily-question

When your heart is set on something, you get closer to your goal with each passing day.
https://www.webpack.top
MIT License
3 stars 0 forks source link

【Q066】受控组件和非受控组件 #66

Open kangyana opened 2 years ago

kangyana commented 2 years ago

1. 受控组件

行为受 React 状态(state) 控制的表单组件。

const Input = (props) => {
  const { value, onChange } = props;

  return <input value={value} onChange={e => onChange(e.target.value)} />
}

2. 非受控组件

行为不受 React 状态 控制的表单组件,行为完全由用户操作影响。

const Input = () => {
  const [value, setValue] = useState('');

  return <input value={value} onChange={e => setValue(e.target.value)} />
}

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

控制组件行为的 状态 是否由外部传入。

alt 图中蓝色表示 组件,黄色表示 状态

4. 受控又非受控组件

现代组件库,有非常多的组件需要做到既支持受控模式,又支持非受控模式。 例如 antd 涉及 输入值、切换、展开收起 的组件,都需要做到。

kangyana commented 2 years ago

5. 如何实现 受控又非受控组件

解决方案:内外两个状态,手动同步

无论哪种模式都使用 子组件状态 。 当 受控模式 时,将 父组件状态 同步给 子组件状态alt

const Input = (props) => {
  const { value: _value, onChange } = props;
  const [value, setValue] = useState(_value);

  const isControlled =_value !== undefined; // 是否受控

  const handleChange = (e) => {
    // 不受控才改变内部状态
    if (!isControlled) setValue(e.target.value);
    onChange(e.target.value);
  }

  useEffect(() => {
    // 外部值变化,手动同步内部状态
    if (isControlled) setValue(_value);
  }, [isControlled])

  return <input value={value} onChange={handleChange} />
}

仔细看上面的代码,我们会发现在 受控模式 下存在两个问题:

kangyana commented 2 years ago

6. 解决原子性

我们不需要 子组件状态父组件状态 时刻统一。 只需要判断,受控模式 下直接使用 父组件状态 就好了。

alt

这样即使状态的同步存在延迟,子组件使用的值也是最新的。

const Input = (props) => {
  // ...同上面的
  const finalValue = isControlled ? _value : value; // 真正使用的值

  return <input value={finalValue} onChange={handleChange} />
}

7. 解决性能

useEffect 使用 setState,会额外触发一次 子组件 的渲染。

此处 state 的作用是什么?

是否能绕过 state 机制?

可以使用 ref + forceUpdate 的组合。

alt 图中的虚线浅色圆圈表示 ref,刷新图标表示 forceUpdate 函数。

const Input = (props) => {
  const { value: _value, onChange } = props;

  const [flag, forceUpdate] = useReducer(v => v + 1, 0); // 触发渲染用的

  const isControlled =_value !== undefined; // 是否受控

  const stateRef = useRef(_value);
  // 受控模式下,将外部的值同步给 Ref
  if (isControlled) {
    stateRef.current = _value;
  }

  const handleChange = (e) => {
    // 手动同步 Ref
    stateRef.current = e.target.value;
    // 手动触发渲染
    forceUpdate();
    onChange(e.target.value);
  }

  return <input value={stateRef.current} onChange={handleChange} />
}
kangyana commented 2 years ago

7. 抽象与复用

我们可以把 受控又非受控组件 的效果封装为 Hook,供其他表单组件使用。

const usePropsValue = (props) => {
  const { value, onChange, defaultValue } = props;

  const [flag, forceUpdate] = useReducer(v => v + 1, 0); // 触发渲染用的

  const isControlled = useMemo(() => value !== undefined, [value]); // 是否受控

  const stateRef = useRef(isControlled ? value : defaultValue);
  // 受控模式下,将外部的值同步给 Ref
  if (isControlled) {
    stateRef.current = value;
  }

  const setState = (nextValue) => {
    if (nextValue === stateRef.current) return;
    // 手动同步 Ref
    stateRef.current = nextValue;
    // 手动触发渲染
    forceUpdate();
    onChange?.(nextValue);
  }

  return [stateRef.current, setState];
}