Reactコンポネートーー制御か非制御か

こんにちは。mediba でテクノロジー2G にてFEをしております、楊です。

最近のPJ開発中にあったことで、Reactコンポネートの制御か非制御か当時綺麗に解決できなかったので、 振り返って、色々調べた上にまとめたメモです。

コンポネートの制御と非制御

React公式の説明

下手な絵で簡単に説明すると↓

ではどうすれば、非制御であり制御コンポネートでもあるコンポネートを作れるでしょう

最も簡単なやり方ーー内外のStateを持ちながら同期させる

まずは子コンポネート内でStateを持たせて、どんな状態(制御モード・非制御モード)でも自分のStateを使うようにする。 次は制御モードにおいて、内部StateをPropsと同期させれば、問題なさそう!

    const Input: FC<{
      value?: string;
      onChange?: (value: string) => void;
    }> = (props) => {
      const isControlled = props.value !== undefined;

      const [value, setValue] = useState(props.value);
      const handleOnChange = (e) => {
          if (isControlled) {
            setValue(e.target.value);
            props.onChange(e.target.value);
          }
      }
      useEffect(() => {
        if (isControlled) {
          setValue(props.value);
        }
      },[props.value]);
      return (
        <input value={value} onChange={handleOnChange}/>
      );
    };

よくみてみると、制御モードでは気になるところも出てきたね

  • 子コンポネート内のState更新はParentより遅い
  • パフォマンス的によくない、useEffect内でのsetStateの使いなので、余計な再描画が発生してしまう

    解決できそうかな、試してみよう

子コンポネート内のState更新はParentより遅い

これだと簡単に解決できそう、制御モードにおいてPropsから渡してきた値そのまま使えばいい。

const finalVal = isControlled ? props.value : value
const handleOnChange = (e) => {
  if (isControlled) {
      setValue(e.target.value);
      props.onChange(e.target.value);
  }
}
return (
    <input value={finalVal}  onChange={handleOnChange}/>
); 

こうすれば、同期は一歩遅くても、子コンポネートに使ってもらうStateは必ず最新であることを担保できる。

パフォマンス的によくない、useEffect内でのsetStateの使いなので、余計なレンダリングが発生してしまう

useEffect内でのState同期なので、再描画を防げなくて、簡単なコンポネートであれば、パフォマンスの影響は少ないが、コンプレックスなコンポネートには、問題である

ポイントは同期させるタイミングだね、ならどうしよう、、、、

Stateでvalue保存には、setterで同期させた直後に再描画が始まる、もし再描画が制御可能になったら問題ないでしょう。 保存にはRefを使って、強制再描画には 仮のstateを作って

    const [_, setObj] = useState({}) 
    function triggerRendering(){ 
       setObj({})
    }

を使う。そうしたら全体は↓

    const Input: FC<{
      value?: string;
      onChange?: (value: string) => void;
    }> = (props) => {
      const isControlled = props.value !== undefined;
      const stateRef = useRef(props.value);
      if (isControlled) {
        stateRef.current = props.value;
      }

      const [_, setObj] = useState({});
      function triggerRendering() {
        setObj({});
      }

      const handleOnChange = (e: React.ChangeEvent) =>     {
          stateRef.current = e.target.value;
          triggerRendering();
          props.onChange(e.target.value);
        };
      return <input value={stateRef.current}  onChange={handleOnChange}/>;
    };

こうしたことで値の同期による再描画がなくなり、制御同時に非制御のコンポネートが出来上がった。 refとhandleOnChangeの部分を取り出し、hooksにすることで、他のコンポネートに適用することもできるでしょう。


現在medibaではメンバーを大募集しています。

募集・応募ページ

medibaってどんな会社だろうと、興味を持っていただいた方は、カジュアル面談もやっておりますので、お気軽にお申込み頂ければと思います。

カジュアル面談