paint-brush
useImperativeHandle フックを使用して効果的な API を設計し、React をマスターする@socialdiscoverygroup
新しい歴史

useImperativeHandle フックを使用して効果的な API を設計し、React をマスターする

Social Discovery Group11m2024/12/23
Read on Terminal Reader

長すぎる; 読むには

React の useImperativeHandle フックを使用すると、開発者はコンポーネントによって公開されるメソッドとプロパティをカスタマイズして、柔軟性と保守性を高めることができます。これは forwardRef と連携して子コンポーネントのプログラム インターフェースを提供し、子コンポーネントの動作を直接制御できるようにします。ベスト プラクティスには、子ロジックの分離、サードパーティ ライブラリとの統合の簡素化、不適切な依存関係配列などのよくある落とし穴の回避などがあります。このフックを効果的に使用することで、開発者はより効率的なコンポーネントを作成し、アプリ全体のパフォーマンスを向上させることができます。
featured image - useImperativeHandle フックを使用して効果的な API を設計し、React をマスターする
Social Discovery Group HackerNoon profile picture
0-item
1-item

最新の React 開発では、useImperativeHandle フックは、コンポーネントの公開値をカスタマイズし、内部メソッドとプロパティをより細かく制御するための強力な手段です。その結果、より効率的なコンポーネント API によって、製品の柔軟性と保守性が向上します。


この記事では、 Social Discovery Groupチームの洞察により、useImperativeHandle を効果的に使用して React コンポーネントを強化するためのベスト プラクティスについて詳しく説明します。


React は、状態、効果、およびコンポーネント間の相互作用を管理するための多くのフック (公式ドキュメントではこの記事の執筆時点で 17 個のフックについて説明されています) を提供します。


中でも useImperativeHandle は、React バージョン 16.8.0 以降に追加された、子コンポーネントのプログラム インターフェース (API) を作成するための便利なツールです。


useImperativeHandle を使用すると、コンポーネントに渡される ref によって返される内容をカスタマイズできます。これは forwardRef と連携して動作し、ref を子コンポーネントに渡すことを可能にします。


 useImperativeHandle(ref, createHandle, [deps]);
  • ref — コンポーネントに渡される参照。
  • createHandle — ref 経由でアクセス可能になるオブジェクトを返す関数。
  • 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 を使用すると、子コンポーネントは、独自に定義した一連のメソッドを親に提供します。


ref を単純に使用する場合と比較した useImperativeHandle の利点


  1. 子コンポーネントのロジックを分離します。親コンポーネントに必要なメソッドのみを提供できます。
  2. 統合を簡素化: React コンポーネントを、Lottie や Three.js などの直接 DOM アクセスを必要とするライブラリと簡単に統合できるようになります。

高度なシナリオ

アニメーションの例などの高度なシナリオで 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 を返します。


よくある間違いと落とし穴

1.依存関係配列が正しく入力されていない

エラーは必ずしもすぐに気付くわけではありません。たとえば、親コンポーネントがプロパティを頻繁に変更し、古いメソッド (古いデータを含む) が引き続き使用される状況が発生する可能性があります。


エラー例:

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 );


正しいアプローチ:

https://use-imperative-handle.levkovich.dev/deps-is-not-correct/correct

 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


正しいアプローチ:

https://use-imperative-handle.levkovich.dev/deps-empty/correct

 useImperativeHandle(ref, () => { // There is might be a difficult task console.log("useImperativeHandle calculated again"); return { focus: () => {} } }, []); // Array of dependencies is correct


  1. useImperativeHandle内のrefを変更する

ref.current を直接変更すると、React の動作が中断されます。React が ref を更新しようとすると、競合や予期しないエラーが発生する可能性があります。


エラー例:

https://use-imperative-handle.levkovich.dev/ref-modification/wrong


 useImperativeHandle(ref, () => { // ref is mutated directly ref.current = { customMethod: () => console.log("Error") }; });

正しいアプローチ:

https://use-imperative-handle.levkovich.dev/ref-modification/correct


 useImperativeHandle(ref, () => ({ customMethod: () => console.log("Correct"), }));


  1. 初期化される前にメソッドを使用する


ref が既に使用可能であると想定して、useEffect またはイベント ハンドラーから useImperativeHandle 経由で提供されるメソッドを呼び出すと、エラーが発生する可能性があります。メソッドを呼び出す前に、常に現在の値を確認してください。


エラー例:


https://use-imperative-handle.levkovich.dev/before-init/wrong

 const increment = useCallback(() => { childRef.current.increment(); }, [])


正しいアプローチ:

https://use-imperative-handle.levkovich.dev/before-init/correct


 const increment = useCallback(() => { if (childRef.current?.increment) { childRef.current.increment() } }, [])


  1. アニメーションと状態間の同期の問題

useImperativeHandle が状態を同期的に変更するメソッド (アニメーションを開始すると同時にスタイルを変更するなど) を返す場合、視覚的な状態と内部ロジックの間に「ギャップ」が生じる可能性があります。たとえば、効果 (useEffect) を使用して、状態と視覚的な動作の一貫性を確保します。


エラー例:

https://use-imperative-handle.levkovich.dev/state-animation-sync/wrong


 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(); }, }));


正しいアプローチ:


https://use-imperative-handle.levkovich.dev/state-animation-sync/correct

 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 を使用します。


useImperativeHandle フックをマスターすることで、React 開発者はメソッドを選択的に公開し、より効率的で保守しやすいコンポーネントを作成できます。Social Discovery Groupチームが提示したテクニックは、開発者が柔軟性を高め、コンポーネント API を合理化し、アプリ全体のパフォーマンスを向上させるのに役立ちます。


著者: Sergey Levkovich、Social Discovery Group のシニア ソフトウェア エンジニア