🖌️

【React】簡単なお絵かきアプリでState内のオブジェクト更新について学ぶ

2023/12/25に公開

Reactを使いこなせるようになるには、Stateの理解を深めることが不可欠だと思います。
今回は超簡単なお絵かきアプリを作成して、State内のオブジェクトを更新する際の注意点について復習しました。
▼参考
https://ja.react.dev/learn/updating-objects-in-state

完成デモ

▼マウスをドラッグすることで画面に線を描画できるアプリケーションです。

State内のオブジェクトを更新するポイント

Stateのイミュータビリティ

Reactでは、Stateは変更されるべきではない(イミュータブルであるべき)とされています。
つまり、Stateに格納されたオブジェクトを直接変更(ミューテーション)するのではなく、新しいオブジェクトを作成してStateを更新する必要があります。

オブジェクトの直接変更は避ける

例えば、以下のようなStateがあるとします。

const [position, setPosition] = useState({ x: 0, y: 0 });

このpositionオブジェクトを更新する際には、直接変更を避け、新しいオブジェクトを作成する必要があります。

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の定義

  1. drawing: ユーザーが現在ドラッグ中かどうかを追跡するブーリアン(真偽値)です。
  2. lines: 描画された全ての線を保持する配列の配列です。各配列はPointオブジェクトの配列で、一つの線の座標を表します。
  3. currentLine: 現在描画中の線の座標を保持するPointオブジェクトの配列です。

イベントハンドラ

  1. handleMouseDown: ユーザーがマウスボタンを押したときに呼ばれます。drawingtrueに設定し、currentLineに最初の座標点を設定します。
  2. handleMouseMove: マウスが動いている間呼ばれます。drawingtrueの場合、新しい座標点をcurrentLineに追加します。
  3. handleMouseUp: ユーザーがマウスボタンを離したときに呼ばれます。drawingfalseに設定し、完了した線(currentLine)をlinesに追加し、currentLineをリセットします。

SVGの描画

lines配列とcurrentLine配列を使用して、SVGのpolyline要素で線を描画します。
linesは既に描かれた線を表し、drawingtrueの間はcurrentLineを使用して現在描画中の線を表示します。

Stateのイミュータビリティ

currentLinelinesを直接変更するのではなく、新しい配列を作成してStateを更新しています。

もしcurrentLinelinesのような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