はじめに
Reactで親子関係があるコンポーネントで、子がサイズを決定した後、親がそのサイズを利用するケースを考えます。SVGです。
とりあえず動いたんですが、ベストプラクティスかどうか判断できないので、いい方法があったら教えてください。
課題
ピンクの親の中に、黄色の子と水色の子の2つ並べたいです。ピンクの上に2つの子rectが乗っていて、それがわかるように下にちょっとはみ出させています。マージンをつけたくなりますが、本筋から外れたコードが混ざってややこしいのでこれで。
最終的なhtmlはこんな感じ。(イメージです)
<g transform></g>
を使うのは、子コンポーネントは相対位置で描画したいからです。
<svg>
<rect
width="350"
fill="ピンク"
name="親"
/>
<g transform="translate(0,0)">
<rect
x="0"
width="200"
fill="黄色"
name="子1"
onClick="自分のrectの幅を広げるリサイズ"
/>
</g>
<g transform="translate(200,0)">
<rect
x="0"
width="150"
fill="水色"
name="子2"
/>
</g>
</svg>
課題は、子1が何かのタイミングでwidth
が変わったときに、親のwidth
、子2のx
を変化したいことです。もちろん初期も。
最終的なソースは下記
シンプルなだけにしょぼいけど、動くサンプル。
実装
子要素
まず部品となる子要素のコードです。
ポイント
- 子要素のrectのwidthは、
useState
で管理する - 初期値は、
useEffect
の第二引数の依存配列なしで、初回のみonSetSize()
を呼び出す - rectのonClickで自分のサイズの変更があったとき、ハンドラーの関数で、
onSetSize()
を呼び出す
import { useState, useEffect } from "react";
interface Child1_1Props {
onSetSize: (w: number) => void;
}
export function Child1_1({
onSetSize,
}: Child1_1Props) {
const [myWidth, setMyWidth] = useState<number>(200);
useEffect(()=>{
onSetSize(myWidth);
}, []);
function handleOnClick() {
const newWidth: number = myWidth+10;
setMyWidth(newWidth);
onSetSize(newWidth);
}
return (
<>
<rect
x={0}
y={0}
width={myWidth}
height={100}
fill="#FFDA76"
onClick={handleOnClick}
/>
<text
x={0}
y={15}
fontSize={13}
fill="#000"
>
Child1_1, width:{myWidth}
</text>
</>
)
}
myWidth
の値自体と、それをコントロールするhandleOnClick()
からのsetMyWidth()
は、クラスでいうカプセル化の役割を担っていると思います。クラスと違うのは、
- クラスは、親から「widthをくれ!」と言われたら、子はいつでも渡せる
- 関数は、子から「widthが変わった!」と通知する
というトリガーの違いです。この記事のケースでは、クリックされる要素は子要素で、子が計算したwidthを親に伝え反映させるので、このやり方になります。
逆に、子要素の外で決まった情報を子に渡してrectの幅を決めたい場合は、引数のChild1_1Props
で渡します。
両方あるという場合は、両方実装する必要がありますね。
なお、子要素のもう一方の子2、Child1_2もありますが、ほぼ同じなので割愛します。本来は、違いをパラメータで渡すのだろうけど、ここでは課題に集中したいので、パラメータなどに余計な情報を入れたくなかった。
親要素
次に子要素たちを使う親要素のコードです。
ポイント
- 子のwidthを、親も
useState
で持つ- (この部分が冗長で、大変気持ち悪い)
- クラスで実装するとしたら、子インスタンスに都度聞く形か
- 子の
onSetSize()
から、ハンドラーを呼び出して、親が持つchildrenWidth
を更新する -
setState
で変更が検知されると、再描画が走り、<g transform></g>
が再計算されて、子2の位置が右にずれる -
setChildrenWidth()
を呼び出すときは、setChildrenWidth(childrenWidth)
としてしまうと、変更が検知されず、再描画されない- 配列インスタンスが再作成されないため(イメージ的には、C言語でいう、アドレス
&childrenWidth
が変わらないと発火しない) - 新しい配列を作るため、
[...childrenWidth]
としている
- 配列インスタンスが再作成されないため(イメージ的には、C言語でいう、アドレス
-
<svg>
は、width
を指定しないと、初期の要素が入るサイズに固定され、子要素のwidth
が広がっても<svg>
のwidth
が変わない。その結果、フレームの外の見えない領域が更新される感じになってしまうので、width={childrenWidth.reduce((acc, w)=>acc+w, 0)}
と、childrenWidth
の合計を計算している
import { useState } from 'react';
import { Child1_1 } from './Child1_1';
import { Child1_2 } from './Child1_2';
export function Parent1() {
const [childrenWidth, setChildrenWidth] = useState<number[]>([0, 0]);
function handleOnSetSizeChild(w: number, i: number) {
console.log({w});
childrenWidth[i] = w;
// 配列を作り直さないと、useStateで変化を検知せず、redrawされない
setChildrenWidth([...childrenWidth]);
}
console.log("redraw");
return (
<>
{/* svgの幅はchildrenWidthが変わるたびに計算しないと
初期値の幅のままになる */}
<svg
width={childrenWidth.reduce((acc, w)=>acc+w, 0)}
>
<rect
x={0}
y={0}
width={childrenWidth.reduce((acc, w)=>acc+w, 0)}
height={150}
fill="#FF8C9E"
/>
<g
transform={`translate(0, 0)`}
>
<Child1_1
onSetSize={(w) => handleOnSetSizeChild(w, 0)}
/>
</g>
<g
transform={`translate(${childrenWidth[0]}, 0)`}
>
<Child1_2
onSetSize={(w) => handleOnSetSizeChild(w, 1)}
/>
</g>
</svg>
</>
);
}
以上です
まとめ
ReactでSVG要素を描くとき、よく出くわすパターンと思うんですが、私の検索能力がしょぼくていまいちいい記事に出会えないです。ひとまず、こうすればできる解をひねり出しましたが、もっとスマートに書けたら嬉しいです。特に親子でwidthを二重持ちしているところ・・・。
いい方法がありましたら、是非コメント欄で教えてください!
関係ないけど近況報告
最近、CRA(create-react-app)から、viteで作るようにしています。TypeScriptの周りでちょいちょい引っ掛かることがあって、「あーもうめんどくさいから最初から作り直したい!」というときに、まるっきり別の方法(=vite)にしてみたら解決できたということからですかね。
ありがたみとかはまだ全然わかっていませんが、変な引っ掛かりはあまりありません。vite@latestでアプリを作成するときに必ずdeprecatedのエラーが出ることくらい。
これもみんなどうしているのか、すごく興味があるので、何かあったらコメント欄で是非!