Reactで子が計算したサイズを親に反映させる

# はじめに
Reactで親子関係があるコンポーネントで、子がサイズを決定した後、親がそのサイズを利用するケースを考えます。SVGです。
とりあえず動いたんですが、ベストプラクティスかどうか判断できないので、いい方法があったら教えてください。

## 課題
ピンクの親の中に、黄色の子と水色の子の２つ並べたいです。ピンクの上に２つの子rectが乗っていて、それがわかるように下にちょっとはみ出させています。マージンをつけたくなりますが、本筋から外れたコードが混ざってややこしいのでこれで。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/cff2b4c9-9267-e6ac-5129-d0f5aa85a1db.png)

最終的な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>
```

課題は、子１が何かのタイミングで`width`が変わったときに、親の`width`、子２の`x`を変化したいことです。もちろん初期も。

最終的なソースは下記

https://github.com/yo16/react_prac_related_size

シンプルなだけにしょぼいけど、動くサンプル。

https://small-pieces-sandbox.onrender.com/contents/react_prac_related_size/



# 実装
## 子要素
まず部品となる子要素のコードです。

ポイント
- 子要素のrectのwidthは、`useState`で管理する
- 初期値は、`useEffect`の第二引数の依存配列なしで、初回のみ`onSetSize()`を呼び出す
- rectのonClickで自分のサイズの変更があったとき、ハンドラーの関数で、`onSetSize()`を呼び出す
```TypeScript: Child1_1.tsx（子１）
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`で渡します。
両方あるという場合は、両方実装する必要がありますね。

なお、子要素のもう一方の子２、Child1_2もありますが、ほぼ同じなので割愛します。本来は、違いをパラメータで渡すのだろうけど、ここでは課題に集中したいので、パラメータなどに余計な情報を入れたくなかった。

## 親要素
次に子要素たちを使う親要素のコードです。

ポイント
- 子のwidthを、親も`useState`で持つ
    - （この部分が冗長で、大変気持ち悪い）
    - クラスで実装するとしたら、子インスタンスに都度聞く形か
- 子の`onSetSize()`から、ハンドラーを呼び出して、親が持つ`childrenWidth`を更新する
- `setState`で変更が検知されると、再描画が走り、`<g transform></g>`が再計算されて、子２の位置が右にずれる
- `setChildrenWidth()`を呼び出すときは、`setChildrenWidth(childrenWidth)`としてしまうと、変更が検知されず、再描画されない
    - 配列インスタンスが再作成されないため（イメージ的には、C言語でいう、アドレス`&childrenWidth`が変わらないと発火しない）
    - 新しい配列を作るため、`[...childrenWidth]`としている
- `<svg>`は、`width`を指定しないと、初期の要素が入るサイズに固定され、子要素の`width`が広がっても`<svg>`の`width`が変わない。その結果、フレームの外の見えない領域が更新される感じになってしまうので、`width={childrenWidth.reduce((acc, w)=>acc+w, 0)}`と、`childrenWidth`の合計を計算している
```TypeScript: Parent1.tsx
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のエラーが出ることくらい。

これもみんなどうしているのか、すごく興味があるので、何かあったらコメント欄で是非！