React: 簡単なお絵かきアプリで State内のオブジェクト更新について学ぶ
React初心者の方はぜひ参考にしてください!
完成デモ
▼マウスをドラッグすることで画面に線を描画できるアプリケーションです。
State内のオブジェクトを更新するポイント
Stateのイミュータビリティ
Reactでは、Stateは変更されるべきではない(イミュータブルであるべき)とされています。
つまり、Stateに格納されたオブジェクトを直接変更(ミューテーション)するのではなく、新しいオブジェクトを作成してStateを更新する必要があります。
オブジェクトの直接変更は避ける
例えば、以下のようなStateがあるとします。
const [position, setPosition] = useState({ x: 0, y: 0 });
このposition
オブジェクトを更新する際には、直接変更を避け、新しいオブジェクトを作成する必要があります。
const [position, setPosition] = useState({ x: 0, y: 0 });
setPosition({ x: newPositionX, y: newPositionY });
この書き方は、プロパティが多い場合、毎回すべてを明示的に記述する必要があり、コードが冗長になるため、スプレッド演算子を使った更新が一般的です。
スプレッド演算子を使ったオブジェクトの更新
setPosition(prevPosition => ({
...prevPosition,
x: newPositionX
}));
この方法では、prevPosition
の他のプロパティはそのまま保持され、x
のみが新しい値に更新されます。
完成コード
今回のアプリケーションのコードは以下のとおりです。
import { useState } from "react";
interface Point {
x: number;
y: number;
}
const Drow = () => {
const [drawing, setDrawing] = useState(false);
const [lines, setLines] = useState<Point[][]>([]);
const [currentLine, setCurrentLine] = useState<Point[]>([]);
const handleMouseDown = (e) => {
setDrawing(true);
setCurrentLine([{ x: e.clientX, y: e.clientY }]);
};
const handleMouseMove = (e) => {
if (!drawing) return;
setCurrentLine(currentLine => [...currentLine, { x: e.clientX, y: e.clientY }]);
};
const handleMouseUp = () => {
if (!drawing) return;
setDrawing(false);
setLines(lines => [...lines, currentLine]);
setCurrentLine([]);
};
return (
<>
<div className="text-center mt-10 pb-4 border-b-2">
<h1>お絵かきアプリでState内のオブジェクト更新を学ぶ</h1>
</div>
<div
role="button"
tabIndex={0}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
className="w-full h-full fixed top-0 left-0"
>
{lines.map((line, index) => (
<svg key={index} className="absolute top-0 left-0 w-full h-full pointer-events-none">
<polyline
fill="none"
stroke="black"
strokeWidth="2"
points={line.map(point => `${point.x},${point.y}`).join(" ")}
/>
</svg>
))}
{drawing && (
<svg className="absolute top-0 left-0 w-full h-full pointer-events-none">
<polyline
fill="none"
stroke="black"
strokeWidth="2"
points={currentLine.map(point => `${point.x},${point.y}`).join(" ")}
/>
</svg>
)}
</div>
</>
);
};
export default Drow;
Stateの定義
-
drawing
: ユーザーが現在ドラッグ中かどうかを追跡するブーリアン(真偽値)です。 -
lines
: 描画された全ての線を保持する配列の配列です。各配列はPoint
オブジェクトの配列で、一つの線の座標を表します。 -
currentLine
: 現在描画中の線の座標を保持するPoint
オブジェクトの配列です。
イベントハンドラ
-
handleMouseDown
: ユーザーがマウスボタンを押したときに呼ばれます。drawing
をtrue
に設定し、currentLine
に最初の座標点を設定します。 -
handleMouseMove
: マウスが動いている間呼ばれます。drawing
がtrue
の場合、新しい座標点をcurrentLine
に追加します。 -
handleMouseUp
: ユーザーがマウスボタンを離したときに呼ばれます。drawing
をfalse
に設定し、完了した線(currentLine
)をlines
に追加し、currentLine
をリセットします。
SVGの描画
lines
配列とcurrentLine
配列を使用して、SVGのpolyline
要素で線を描画します。
lines
は既に描かれた線を表し、drawing
がtrue
の間はcurrentLine
を使用して現在描画中の線を表示します。
Stateのイミュータビリティ
currentLine
やlines
を直接変更するのではなく、新しい配列を作成してStateを更新しています。
もしcurrentLine
やlines
のようなStateを直接変更してしまうと、Reactはその変更を検出できなくなります。
例えば、次のようなコードは推奨されません:
const handleMouseMove = (e) => {
if (!drawing) return;
// これはStateを直接変更している例
currentLine.push({ x: e.clientX, y: e.clientY });
setCurrentLine(currentLine);
};
このコードでは、push
メソッドを使ってcurrentLine
配列に直接新しい要素を追加しています。
この操作は配列の元の参照を変更しないため、Reactはこの変更を検出できず、結果としてコンポーネントは再レンダリングされません。
代わりに、常に新しいオブジェクトや配列を作成してStateを更新するべきです。例えば、handleMouseMove
は次のように書かれるべきです:
const handleMouseMove = (e) => {
if (!drawing) return;
// 新しい配列を作成してStateを更新
setCurrentLine(currentLine => [...currentLine, { x: e.clientX, y: e.clientY }]);
};
currentLine
の既存の要素を新しい配列に展開し、新しい点を追加しています。
これにより、配列の新しいインスタンスが作成され、Reactはこの変更を検出し、適切にコンポーネントを再レンダリングします。
まとめ
今回は簡単なお絵かきアプリケーションを作成して、ReactのState管理とStateのイミュータビリティ(不変性)の重要性について、解説しました!
Discussion