🎆

Reactの今まであまり触れてこなかった機能について試したことのまとめ

2022/08/30に公開

react18.2で検証

createPortal

以下はドキュメントの引用

ポータル (portal) は、親コンポーネントの DOM 階層外にある DOM ノードに対して子コンポーネントをレンダーするための公式の仕組みを提供します。

ポータルを使うと<div id="app">以下に書かれた<Modal>コンポーネントがDOM上では<div id="portal">以下にレンダリングされる。ただし、イベントは<div id="app">にバブリング(子要素で発生したイベントが親要素に伝搬)する。

tsx
import { FC, useState, ReactNode, useEffect } from "react";
import { createPortal } from "react-dom";

const Modal: FC<{ children: ReactNode }> = ({ children }) => {
  const target = document.querySelector("#portal");
  return createPortal(children, target!);
};

const Panrent = () => {
  const [prepared, setPrepared] = useState(false);

  useEffect(() => setPrepared(true), []);

  return (
    <>
      <div id="portal" onClick={
        // ChildのButtonをクリックしても呼ばれない
        () => console.log("in portal")
       } />

      <div id="app" onClick={
        // ChildのButtonをクリックすると呼ばれる
        () => console.log("outside portal")
      }>
        {prepared && (
          <Modal>
            <Child />
          </Modal>
        )}
      </div>
    </>
  );
};

const Child = () => {
  return <button>Click</button>;
};

export default Panrent;

useRef

以下はドキュメントの引用

ref のことを DOM にアクセスする手段として理解しているかもしれません。<div ref={myRef} /> のようにして React に ref オブジェクトを渡した場合、React は DOM ノードに変更があるたびに .current プロパティをその DOM ノードに設定します。

以下はドキュメントに書かれているボタンがクリックされたら、テキストボックスにフォーカスを当てるサンプル

tsx
export function TextInputWithFocusButton() {
  const inputEl = useRef<HTMLInputElement>(null!);
  const onButtonClick = () => {
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

export default TextInputWithFocusButton;

さらに以下のように書かれている

useRef()はref属性で使うだけではなく、より便利に使えます。これはクラスでインスタンス変数を使うのと同様にして、あらゆる書き換え可能な値を保持しておくのに便利です。(中略) useRefは中身が変更になってもそのことを通知しないということを覚えておいてください。.currentプロパティを書き換えても再レンダーは発生しません。DOMノードをrefに割り当てたり割り当てを解除したりする際に何らかのコードを走らせたいという場合は、コールバックrefを代わりに使用してください。

以下のサンプルで動作確認

tsx
import { useState, useRef } from "react";

const Ref = () => {
  const [count, setCount] = useState(0);
  const countRef = useRef(0);

  return (
    <>
      <div>count:{count}</div>
      <div>countRef:{countRef.current}</div>
      <button onClick={() => setCount((p) => p + 1)}>+count</button>
      <button onClick={() => countRef.current++}>+countRef</button>
    </>
  );
};

export default Ref;
  1. +countボタンをクリック → count:1、countRef:0が表示される
  2. +countRefボタンをクリック → 画面の表示は変わらない
  3. +countボタンをクリック → count:2、countRef:1が表示される

上の例から、countは値を更新するたびに画面が再描画され、countRefは値を更新しても画面が再描画されないことがわかる。コンポーネントが再描画されても保持したい値でかつ値を更新しても画面を再描画する必要がない値を保持するのに使用することができる。

forwardRef

以下はドキュメントの引用

React.forwardRef は ref を配下のツリーの別のコンポーネントに受け渡す React コンポーネントを作成します

以下はドキュメントのuseRefの項のサンプルコードの<input>部分をコンポーネント化して、forwardRefを使って親から子へrefを渡せるようにしたコード

tsx
export const Input = forwardRef<HTMLInputElement>((_, ref) => {
  return <input ref={ref} type="text" />;
});

export function TextInputWithFocusButton() {
  const inputEl = useRef<HTMLInputElement>(null!);
  const onButtonClick = () => {
    inputEl.current.focus();
  };
  return (
    <>
      <Input ref={inputEl} />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

export default TextInputWithFocusButton;

また、以下のようにforwardRefを使用せずに、子コンポーネントに直接refを渡そうとするとエラーになる(以下はドキュメントforwardRefの項のサンプルコードをいじったもの)

jsx
const FancyButton = (props, ref) => {(
  <button ref={ref}>
    {props.children}
  </button>
)};

useImperativeHandle

以下はドキュメントの引用

useImperativeHandleはrefが使われた時に親コンポーネントに渡されるインスタンス値をカスタマイズするのに使います

ドキュメントのuseImperativeHandleの項の、子コンポーネントが公開するfocusメソッドを親コンポーネントが呼び出すサンプルコード。コンポーネントにメソッドを追加するときに使用する。

tsx
interface Handler {
  focus(): void;
}

export const Input = forwardRef<Handler>((_, ref) => {
  const inputRef = useRef<HTMLInputElement>(null!);

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
  }));

  return <input ref={inputRef} type="text" />;
});

export function TextInputWithFocusButton() {
  const inputEl = useRef<Handler>(null!);
  const onButtonClick = () => {
    inputEl.current.focus();
  };
  return (
    <>
      <Input ref={inputEl} />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

export default TextInputWithFocusButton;

useLayoutEffect

以下はドキュメントの引用

DOMからレイアウトを読み出して同期的に再描画を行う場合に使ってください。useLayoutEffectの内部でスケジュールされた更新はブラウザによって描画される前のタイミングで同期的に処理されます。

試してないが、描画(再描画)される前に何かしたいときにここに処理を書く。useEffectよりも先に実行される

useTransition

以下はドキュメントの引用

トランジションの実行中状態を表す状態値と、トランジションを開始するための関数を返します。
isPendingはトランジションがアクティブかどうかを表しており、ユーザに保留中状態を表示するのに使えます

以下はドキュメントのサンプルコードを少しいじったもの。ボタンをクリックすると時間がかかるsetCountの処理が終了するまで画面にPendingが表示される

tsx
import { useState, useTransition } from "react";

const Transition = () => {
  const [isPending, startTransition] = useTransition();
  const [count, setCount] = useState(0);

  const handleClick = () => {
    startTransition(() => {
      setCount((c) => {
        for (let i = 0; i < 100000000; i++) {
          // 時間がかかる処理
        }
        return c + 1;
      });
    });
  };

  return (
    <div>
      {isPending && <>Pending</>}
      <button onClick={handleClick}>{count}</button>
    </div>
  );
};

export default Transition;

また、以下の様に補足されている

トランジション内での更新はクリックのような緊急性の高い更新がある場合は遅延されることがあります。トランジション内での更新によってコンテンツが再サスペンドした場合でもフォールバックは表示されません。これにより更新後のデータをレンダーしている最中に、ユーザが現在のコンテンツを操作しつづけられるようになります。

以下は、0〜10000の数字を奇数または偶数でフィルターするサンプルコード。startTransitionを介さず直接setValue実行するとラジオボタンの操作性が悪くなることが確認できる。

tsx
import { useState, useTransition, ChangeEvent } from "react";

const numbers = Array(10000)
  .fill(null)
  .map((_, i) => i);

const check = (type: string, num: number): boolean => {
  if (type == "even") return num % 2 == 0;
  else if (type == "odd") return num % 2 != 0;
  else return true;
};

const Transition = () => {
  const [_, startTransition] = useTransition();
  const [value, setValue] = useState("");
  const onChange = (e: ChangeEvent<HTMLInputElement>) => {
    // setValueを直接実行すると、ラジオボタンの反応が悪くなる
    // setValue(e.target.value); 
    startTransition(() => setValue(e.target.value));
  };

  return (
    <>
      <div onChange={onChange}>
        偶数<input type="radio" name="value" value="even" />
        奇数<input type="radio" name="value" value="odd" />
      </div>
      <ul>
        {numbers
          .filter((n) => check(value, n))
          .map((n) => (
            <li key={n}>{n}</li>
          ))}
      </ul>
    </>
  );
};

useDeferredValue

以下はドキュメントの引用

useDeferredValueは値を受け取りその値のコピーを返しますが、返り値はより緊急性の高い更新がある場合に遅延されうるようになっています。現在のレンダーがユーザ入力のような緊急性の高い更新である場合には、React は前回と同じ値を返し、新しい値でのレンダーは緊急性の高いレンダーが完了した後に行うようにします。(中略) useDeferredValue を使う利点は、(常に何らかの固定の時間待つのではなく)他の作業が終わった時点ですぐに React が更新を処理できるという点と、startTransition と同様に値を遅延させることで既存のコンテンツがふいにフォールバックに隠されてしまわないよう待機できる

useTransitionの項のサンプルコードをuseDeferredValueで書き直したコード。useDeferredValueを介さず直接filterNumbersをレンダーするとラジオボタンの操作性が悪くなることが確認できる。

tsx
const Deffered = () => {
  const [value, setValue] = useState("");

  const onChange =
    (e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value);

  const filterNumbers = numbers.filter((n) => check(value, n)).map((n) => <li key={n}>{n}</li>);

  const deferredNumbers = useDeferredValue(filterNumbers);

  return (
    <>
      <div onChange={onChange}>
        偶数<input type="radio" name="value" value="even" />
        奇数<input type="radio" name="value" value="odd" />
      </div>
      {/* filterNumbersを直接レンダーすると重くなる */}
      <ul>{deferredNumbers}</ul>
    </>
  );
};
export default Deffered;

Discussion