React的受控与非受控组件

Shunlin
10分钟
前端React
React的受控与非受控组件

受控与非受控

  • 受控,组件或者dom的value受到外部的state控制,由代码来控制它的value的变化

好处是组件的value完全由代码来控制,最灵活。然而由于React的组件渲染机制,state的改变还会触发组件的重新渲染,影响性能

function App() {
  const [value, setValue] = useState("");

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

  • 非受控,由组件或者dom自己维护自己的value,需要的时候再取出来

value交由组件或者dom节点自己维护,需要的时候再取出来用,即减少了定义state,自己处理state的复杂度,也能再一定程度上减少组件的频繁渲染

function App() {
  const inputRef = useRef<HTMLInputElement>(null);
  return (
    <>
      <input
        ref={inputRef}
        onChange={(e) => {
          console.log("e.value", e.target.value);
          // setValue(e.target.value);
        }}
      />
    </>
  );
}

export default App;

然而事实上受控与非受控不是孰好孰坏的排斥关系,而是需要根据实际场景选择是否完全控制这个组件。因此在很多自己封装的组件中,props往往会有三个值

interface Props {
  value?: Value;
  defaultValue?: Value;
  onChange?: (value: Value) => void;
}

当value不传入的时候,组件自己维护内部的state(非受控)反正为受控组件。这个逻辑在组件内部依靠对value是否为undefined来判断。

例子: Conter组件

这个例子中的重点就是判断propsValue是否undefined, 根据这个条件来选择使用哪个value

interface CounterProps {
  value?: number;
  defaultValue?: number;
  onChange?: (value: number) => void;
}

function Counter(props: CounterProps) {
  const { value: propsValue, defaultValue, onChange } = props;
  const isFirstRender = useRef(true);

  const DEFAULT_FALUE = 0;

  const [value, setValue] = useState<number>(() => {
    if (propsValue !== undefined) {
      return propsValue;
    }
    if (defaultValue !== undefined) {
      return defaultValue;
    }
    return DEFAULT_FALUE;
  });

  useEffect(() => {
    // 受控组件切换到非受控时做处理,将value充值为undefined
    if (propsValue === undefined && !isFirstRender.current) {
      setValue(propsValue!);
    }
    isFirstRender.current = false;
  }, [propsValue]);

  const mergedValue = propsValue === undefined ? value : propsValue;

  const onAdd = () => {
    const updatedValue = mergedValue + 1;
    if (propsValue === undefined) {
      setValue(updatedValue);
    }
    onChange?.(updatedValue);
  };

  const onSubtract = () => {
    const updatedValue = mergedValue - 1;
    if (propsValue === undefined) {
      setValue(updatedValue);
    }
    onChange?.(updatedValue);
  };

  return (
    <div
      style={{
        display: "flex",
        gap: "10px",
      }}
    >
      <button onClick={onAdd}>+</button>
      <div>{mergedValue}</div>
      <button onClick={onSubtract}>-</button>
    </div>
  );
}

function App() {
  const inputRef = useRef<HTMLInputElement>(null);
  const [count, setCount] = useState(1);
  return (
    <>
      <Counter
        defaultValue={123}
        value={count}
        onChange={(c) => {
          console.log("change", c);
          setCount(c);
        }}
      />
    </>
  );
}

export default App;

Hook封装

更近一步,可以封装出一个支持受控与非受控的hook

(下面代码摘自 掘金React通关秘籍

function useMergeState<T>(
  defaultStateValue: T,
  props?: {
    defaultValue?: T,
    value?: T,
    onChange?: (value: T) => void;
  },
): [T, React.Dispatch<React.SetStateAction<T>>,] {
  const { defaultValue, value: propsValue, onChange } = props || {};

  const isFirstRender = useRef(true);

  const [stateValue, setStateValue] = useState<T>(() => {
    if (propsValue !== undefined) {
      return propsValue!;
    } else if(defaultValue !== undefined){
      return defaultValue!;
    } else {
      return defaultStateValue;
    }
  });

  useEffect(() => {
    if(propsValue === undefined && !isFirstRender.current) {
      setStateValue(propsValue!);
    }

    isFirstRender.current = false;
  }, [propsValue]);

  const mergedValue = propsValue === undefined ? stateValue : propsValue;

  function isFunction(value: unknown): value is Function {
    return typeof value === 'function';
  } 

  const setState = useCallback((value: SetStateAction<T>) => {
    let res = isFunction(value) ? value(stateValue) : value

    if (propsValue === undefined) {
      setStateValue(res);
    }
    onChange?.(res);
  }, [stateValue]);

  return [mergedValue, setState]
}

也可以直接使用ahook封装好的hookuseControllableValue

💬评论交流

交流的意思是沟通一下,不是吵架。吵架的意思是大声地沟通一下,不是准备打架