Reactの今まであまり触れてこなかった機能について試したことのまとめ
react18.2で検証
createPortal
以下はドキュメントの引用
ポータル (portal) は、親コンポーネントの DOM 階層外にある DOM ノードに対して子コンポーネントをレンダーするための公式の仕組みを提供します。
ポータルを使うと<div id="app">
以下に書かれた<Modal>
コンポーネントがDOM上では<div id="portal">
以下にレンダリングされる。ただし、イベントは<div id="app">
にバブリング(子要素で発生したイベントが親要素に伝搬)する。
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 ノードに設定します。
以下はドキュメントに書かれているボタンがクリックされたら、テキストボックスにフォーカスを当てるサンプル
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を代わりに使用してください。
以下のサンプルで動作確認
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;
-
+count
ボタンをクリック →count:1、countRef:0
が表示される -
+countRef
ボタンをクリック → 画面の表示は変わらない -
+count
ボタンをクリック →count:2、countRef:1
が表示される
上の例から、count
は値を更新するたびに画面が再描画され、countRef
は値を更新しても画面が再描画されないことがわかる。コンポーネントが再描画されても保持したい値でかつ値を更新しても画面を再描画する必要がない値を保持するのに使用することができる。
forwardRef
以下はドキュメントの引用
React.forwardRef は ref を配下のツリーの別のコンポーネントに受け渡す React コンポーネントを作成します
以下はドキュメントのuseRef
の項のサンプルコードの<input>
部分をコンポーネント化して、forwardRef
を使って親から子へref
を渡せるようにしたコード
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
の項のサンプルコードをいじったもの)
const FancyButton = (props, ref) => {(
<button ref={ref}>
{props.children}
</button>
)};
useImperativeHandle
以下はドキュメントの引用
useImperativeHandleはrefが使われた時に親コンポーネントに渡されるインスタンス値をカスタマイズするのに使います
ドキュメントのuseImperativeHandle
の項の、子コンポーネントが公開するfocus
メソッドを親コンポーネントが呼び出すサンプルコード。コンポーネントにメソッドを追加するときに使用する。
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
が表示される
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
実行するとラジオボタンの操作性が悪くなることが確認できる。
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
をレンダーするとラジオボタンの操作性が悪くなることが確認できる。
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