はじめに
DOMのバウンディングボックスなどを取得する場合、hooks
のuseRef
を使いたいケースがあります。しかし、下位のコンポーネントにそれらの情報を渡す場合などは、注意が必要になります。
例
次のようにメッセージの右下にいいねボタンを表示したいとします。
うまくいかないパターン
ThumbsUp
コンポーネントに対してボタン位置の基準となるHTMLElement
を渡し、ThumbsUp
にてボタンの表示位置を求めています。
App.tsx
import React, { useRef } from "react";
import ThumbsUp from "./ThumbsUp";
import "./styles.scss";
export default function App() {
const ref = useRef<HTMLSpanElement | null>();
return (
<div>
<span ref={ref} className="baloon">
こんにちは
</span>
{ref.current && (
<ThumbsUp anchor={ref.current} />
)}
</div>
);
}
ThumbsUp.tsx
import React, { useRef, useEffect } from "react";
import "./styles.scss";
type Props = {
anchor: HTMLElement;
};
export default function Portal({ anchor }: Props) {
const ref = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (ref.current) {
const anchorRect = anchor.getBoundingClientRect();
const position = {
top: anchorRect.top + anchorRect.height - 20,
left: anchorRect.left + anchorRect.width - 10
};
ref.current.style.top = `${position.top}px`;
ref.current.style.left = `${position.left}px`;
}
}, [anchor]);
return (
<div ref={ref} className="thumbs-up">
<button>
<span role="img" aria-label="thumbs-up">
👍
</span>
</button>
</div>
);
}
何が起こるか
残念ながら、意図通りにいいねボタンが表示されません。
これは公式にも記載されていますが、App.tsx
において、ref.current
が置き換わっても再レンダーは発生しないためです。そのため、ThumbsUp
はレンダリングされないままの状態となっています。
解決策
コールバック式のref
とuseState
を使うことで解決することができます。下のようにspan
のref
に関数を与えることで、setAnchor
が呼ばれたタイミングで再レンダーを発生させ、意図通りThumbsUp
をレンダリングできるようになります。
App.tsx
import React, { useState } from "react";
import ThumbsUp from "./ThumbsUp";
import "./styles.scss";
export default function App() {
const [anchor, setAnchor] = useState<HTMLSpanElement>();
return (
<div>
<span
ref={elm => { // コールバック関数を与える
setAnchor(elm);
}}
className="baloon"
>
こんにちは
</span>
{anchor && <ThumbsUp anchor={anchor} /> }
</div>
);
}