最新の React 開発では、useImperativeHandle フックは、コンポーネントの公開値をカスタマイズし、内部メソッドとプロパティをより細かく制御するための強力な手段です。その結果、より効率的なコンポーネント API によって、製品の柔軟性と保守性が向上します。
この記事では、 Social Discovery Groupチームの洞察により、useImperativeHandle を効果的に使用して React コンポーネントを強化するためのベスト プラクティスについて詳しく説明します。
React は、状態、効果、およびコンポーネント間の相互作用を管理するための多くのフック (公式ドキュメントではこの記事の執筆時点で 17 個のフックについて説明されています) を提供します。
中でも useImperativeHandle は、React バージョン 16.8.0 以降に追加された、子コンポーネントのプログラム インターフェース (API) を作成するための便利なツールです。
useImperativeHandle を使用すると、コンポーネントに渡される ref によって返される内容をカスタマイズできます。これは forwardRef と連携して動作し、ref を子コンポーネントに渡すことを可能にします。
useImperativeHandle(ref, createHandle, [deps]);
このフックを使用すると、コンポーネントの動作を外部から制御できます。これは、サードパーティのライブラリ、複雑なアニメーション、メソッドへの直接アクセスが必要なコンポーネントの操作など、特定の状況で役立ちます。ただし、React の宣言型アプローチに違反するため、慎重に使用する必要があります。
子コンポーネントの DOM を操作する必要があるとします。ref を使用してこれを行う方法の例を次に示します。
import React, { forwardRef, useRef } from 'react'; const CustomInput = forwardRef((props, ref) => { // Use forwardRef to pass the ref to the input element return <input ref={ref} {...props} />; }); export default function App() { const inputRef = useRef(); const handleFocus = () => { inputRef.current.focus(); // Directly controlling the input }; const handleClear = () => { inputRef.current.value = ''; // Directly controlling the input value }; return ( <div> <CustomInput ref={inputRef} /> <button onClick={handleFocus}>Focus</button> <button onClick={handleClear}>Clear</button> </div> ); }
useImperativeHandle を使用してこれを実現する方法は次のとおりです。
import React, { useImperativeHandle, forwardRef, useRef } from 'react'; const CustomInput = forwardRef((props, ref) => { const inputRef = useRef(); useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); }, clear: () => { inputRef.current.value = ''; }, })); return <input ref={inputRef} {...props} />; }); export default function App() { const inputRef = useRef(); return ( <div> <CustomInput ref={inputRef} /> <button onClick={inputRef.current.focus}>Focus</button> <button onClick={inputRef.current.clear}>Clear</button> </div> ); }
上記の例に見られるように、useImperativeHandle を使用すると、子コンポーネントは、独自に定義した一連のメソッドを親に提供します。
アニメーションの例などの高度なシナリオで useImperativeHandle を使用すると、コンポーネント内の複雑な動作を分離できます。これにより、特にアニメーションやサウンド ライブラリを使用する場合に、親コンポーネントがよりシンプルで読みやすくなります。
import React, { useRef, useState, useImperativeHandle, forwardRef, memo } from "react"; import { Player } from '@lottiefiles/react-lottie-player' import animation from "./animation.json"; const AnimationWithSound = memo( forwardRef((props, ref) => { const [isAnimating, setIsAnimating] = useState(false); const animationRef = useRef(null); const targetDivRef = useRef(null); useImperativeHandle( ref, () => ({ startAnimation: () => { setIsAnimating(true); animationRef.current?.play() updateStyles("start"); }, stopAnimation: () => { animationRef.current?.stop() updateStyles("stop"); }, }), [] ); const updateStyles = (action) => { if (typeof window === 'undefined' || !targetDivRef.current) return; if (action === "start") { if (targetDivRef.current.classList.contains(styles.stop)) { targetDivRef.current.classList.remove(styles.stop); } targetDivRef.current.classList.add(styles.start); } else if (action === "stop") { if (targetDivRef.current.classList.contains(styles.start)) { targetDivRef.current.classList.remove(styles.start); } targetDivRef.current.classList.add(styles.stop); } }; return ( <div> <Player src={animation} loop={isAnimating} autoplay={false} style={{width: 200, height: 200}} ref={animationRef} /> <div ref={targetDivRef} className="target-div"> This div changes styles </div> </div> ); }) ); export default function App() { const animationRef = useRef(); const handleStart = () => { animationRef.current.startAnimation(); }; const handleStop = () => { animationRef.current.stopAnimation(); }; return ( <div> <h1>Lottie Animation with Sound</h1> <AnimationWithSound ref={animationRef} /> <button onClick={handleStart}>Start Animation</button> <button onClick={handleStop}>Stop Animation</button> </div> ); }
この例では、子コンポーネントは、複雑なロジックを内部にカプセル化するメソッド startAnimation と stopAnimation を返します。
エラーは必ずしもすぐに気付くわけではありません。たとえば、親コンポーネントがプロパティを頻繁に変更し、古いメソッド (古いデータを含む) が引き続き使用される状況が発生する可能性があります。
エラー例:
https://use-imperative-handle.levkovich.dev/deps-is-not-correct/wrong
const [count, setCount] = useState(0); const increment = useCallback(() => { console.log("Current count in increment:", count); // Shows old value setCount(count + 1); // Are using the old value of count }, [count]); useImperativeHandle( ref, () => ({ increment, // Link to the old function is used }), [] // Array of dependencies do not include increment function );
正しいアプローチ:
const [count, setCount] = useState(0); useImperativeHandle( ref, () => ({ increment, }), [increment] // Array of dependencies include increment function );
2. 依存関係配列が見つからない
依存関係配列が提供されていない場合、React は、useImperativeHandle 内のオブジェクトをレンダリングごとに再作成する必要があるものと想定します。特にフック内で「重い」計算が実行される場合、これにより重大なパフォーマンスの問題が発生する可能性があります。
エラー例:
useImperativeHandle(ref, () => { // There is might be a difficult task console.log("useImperativeHandle calculated again"); return { focus: () => {} } }); // Array of dependencies is missing
正しいアプローチ:
useImperativeHandle(ref, () => { // There is might be a difficult task console.log("useImperativeHandle calculated again"); return { focus: () => {} } }, []); // Array of dependencies is correct
ref.current を直接変更すると、React の動作が中断されます。React が ref を更新しようとすると、競合や予期しないエラーが発生する可能性があります。
エラー例:
useImperativeHandle(ref, () => { // ref is mutated directly ref.current = { customMethod: () => console.log("Error") }; });
正しいアプローチ:
useImperativeHandle(ref, () => ({ customMethod: () => console.log("Correct"), }));
ref が既に使用可能であると想定して、useEffect またはイベント ハンドラーから useImperativeHandle 経由で提供されるメソッドを呼び出すと、エラーが発生する可能性があります。メソッドを呼び出す前に、常に現在の値を確認してください。
エラー例:
const increment = useCallback(() => { childRef.current.increment(); }, [])
正しいアプローチ:
const increment = useCallback(() => { if (childRef.current?.increment) { childRef.current.increment() } }, [])
useImperativeHandle が状態を同期的に変更するメソッド (アニメーションを開始すると同時にスタイルを変更するなど) を返す場合、視覚的な状態と内部ロジックの間に「ギャップ」が生じる可能性があります。たとえば、効果 (useEffect) を使用して、状態と視覚的な動作の一貫性を確保します。
エラー例:
useImperativeHandle(ref, () => ({ startAnimation: () => { setState("running"); // Animation starts before the state changes lottieRef.current.play(); }, stopAnimation: () => { setState("stopped"); // Animation stops before the state changes lottieRef.current.stop(); }, }));
正しいアプローチ:
useEffect(() => { if (state === "running" && lottieRef.current) { lottieRef.current.play(); } else if (state === "stopped" && lottieRef.current) { lottieRef.current.stop(); } }, [state]); // Triggered when the state changes useImperativeHandle( ref, () => ({ startAnimation: () => { setState("running"); }, stopAnimation: () => { setState("stopped"); }, }), [] );
useImperativeHandle の使用は、次の状況で正当化されます。
子コンポーネントの動作を制御する:たとえば、複雑な入力コンポーネントにフォーカスまたはリセット メソッドを提供します。
実装の詳細を非表示にする:親コンポーネントは、ref オブジェクト全体ではなく、必要なメソッドのみを受け取ります。
useImperativeHandle を使用する前に、次の質問を自問してください。
useImperativeHandle フックをマスターすることで、React 開発者はメソッドを選択的に公開し、より効率的で保守しやすいコンポーネントを作成できます。Social Discovery Groupチームが提示したテクニックは、開発者が柔軟性を高め、コンポーネント API を合理化し、アプリ全体のパフォーマンスを向上させるのに役立ちます。
著者: Sergey Levkovich、Social Discovery Group のシニア ソフトウェア エンジニア