0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-08-05

はじめに

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

課題

ピンクの親の中に、黄色の子と水色の子の2つ並べたいです。ピンクの上に2つの子rectが乗っていて、それがわかるように下にちょっとはみ出させています。マージンをつけたくなりますが、本筋から外れたコードが混ざってややこしいのでこれで。
image.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>

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

最終的なソースは下記

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

実装

子要素

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

ポイント

  • 子要素のrectのwidthは、useStateで管理する
  • 初期値は、useEffectの第二引数の依存配列なしで、初回のみonSetSize()を呼び出す
  • rectのonClickで自分のサイズの変更があったとき、ハンドラーの関数で、onSetSize()を呼び出す
Child1_1.tsx(子1)
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]としている
  • <svg>は、widthを指定しないと、初期の要素が入るサイズに固定され、子要素のwidthが広がっても<svg>widthが変わない。その結果、フレームの外の見えない領域が更新される感じになってしまうので、width={childrenWidth.reduce((acc, w)=>acc+w, 0)}と、childrenWidthの合計を計算している
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のエラーが出ることくらい。

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?