Closed4

React実践の教科書 サンプルアプリ実装

YamahitsujiYamahitsuji

本の解答ではメモテキストをボタンクリックで追加していたが、今回はformで実装。formとonSubmitにすればテキストフォームでエンターを押した時でも送信ができる。
この際、HTMLFormElementのsubmitイベントはデフォルトでページリフレッシュをかける。このため、submit直後にリフレッシュが走り、ステートに保存しているmemoが初期化されてしまっていた。

このため、preventDefault()でページリフレッシュを防ぐ必要がある。
https://www.tutorialspoint.com/how-to-stop-refreshing-the-page-on-submit-in-javascript
https://react.dev/learn/responding-to-events

YamahitsujiYamahitsuji

ちなみにChatGPT(GPT-4)先生にコードとバグを聞くと、原因と修正したコードを返してくれた。すごい。

以下のReactのコードではformをsubmitした際、ステートが初期化されてしまいます。原因はなんでしょうか?

---
import { FC, ChangeEvent, useState } from "react";
import styled from "styled-components";

export const App: FC = () => {
  const [text, setText] = useState<string>("");
  const [memos, setMemos] = useState<string[]>(["aaa", "bbb"]);

  const onChange = (e: ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value);
  };

  const onSubmit = () => {
    const newMemos = [...memos];
    newMemos.push(text);
    setMemos(newMemos);
    setText("");
  };

  return (
    <div>
      <h1>簡単メモアプリ</h1>
      <form onSubmit={onSubmit}>
        <input type="text" value={text} onChange={onChange} />
        <input type="submit" value="追加" />
      </form>

      <div>
        <p>メモ一覧</p>
        <ul>
          {memos.map((memo, i) => {
            return (
              <li key={i}>
                <div>
                  <p>{memo}</p>
                  <button>削除</button>
                </div>
              </li>
            );
          })}
        </ul>
      </div>
    </div>
  );
};

返答↓

このReactのコードでは、フォームがsubmitされるとページがリフレッシュされることが原因でステートが初期化されてしまいます。

HTMLのフォームがsubmitされると、通常、ブラウザはページをリフレッシュ(または新しいページに遷移)します。Reactでは、このデフォルトの動作が発生するとアプリケーションの状態が初期化されます。

これを防ぐためには、`onSubmit`イベントハンドラの中で`event.preventDefault()`を呼び出すことでデフォルトのフォーム送信動作をキャンセルします。

以下に修正したコードを示します:
import { FC, ChangeEvent, useState, FormEvent } from "react";
import styled from "styled-components";

export const App: FC = () => {
  const [text, setText] = useState<string>("");
  const [memos, setMemos] = useState<string[]>(["aaa", "bbb"]);

  const onChange = (e: ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value);
  };

  const onSubmit = (e: FormEvent) => {
    e.preventDefault(); // この行を追加

    const newMemos = [...memos];
    newMemos.push(text);
    setMemos(newMemos);
    setText("");
  };

  return (
    <div>
      <h1>簡単メモアプリ</h1>
      <form onSubmit={onSubmit}>
        <input type="text" value={text} onChange={onChange} />
        <input type="submit" value="追加" />
      </form>

      <div>
        <p>メモ一覧</p>
        <ul>
          {memos.map((memo, i) => {
            return (
              <li key={i}>
                <div>
                  <p>{memo}</p>
                  <button>削除</button>
                </div>
              </li>
            );
          })}
        </ul>
      </div>
    </div>
  );
};
この変更により、フォームを送信してもページはリフレッシュされず、Reactアプリケーションの状態も保持されます。
YamahitsujiYamahitsuji

useStateで配列を使用する場合、ステートの変数をsetすることはできなかった。GPT4先生によると以下のような解答。

質問

ReactのuseStateで作成したステートの値を変更してsetしても反映されません。例えば以下のようなコードです。原因はなんでしょうか?
export const App: FC = () => {
  const [memos, setMemos] = useState<string[]>(["aaa", "bbb"]);

  const onMemoDelete = (i: number) => {
    memos.splice(i, 1);
    setMemos(memos);
  };
};

解答

Reactの `useState` フックを使用するとき、それを通じて更新した値が反映されない問題は、大抵Reactがステートの更新を検知できないような方法でステートを変更していることが原因です。

このコードの場合、 `splice` メソッドは元の配列(`memos`)を直接変更します。これは `useState` のステート更新と相性が悪く、新しい値の設定が検出されないため反映が行われません。つまり、`useState`は新旧の値を===で比較します。そのため、配列やオブジェクトを更新する際には新しい配列やオブジェクトを生成する必要があります。

以下のように、ステートを新しい配列として作り直すことでこの問題を解決することができます:
export const App: FC = () => {
  const [memos, setMemos] = useState<string[]>(["aaa", "bbb"]);

  const onMemoDelete = (i: number) => {
    const newMemos = [...memos];  // スプレッド演算子を使って新しい配列を作る
    newMemos.splice(i, 1);
    setMemos(newMemos);
  };
};
この例では、まず `memos` 配列をコピーして新しい配列 `newMemos` を作成します。次に、`splice` メソッドを使って `newMemos` から特定のメモを削除します。最後に、`setMemos` を使ってステートを更新します。

このようにすることで、Reactはステートの更新を適切に検出し、UIを再レンダリングすることができます。

別の方法として、削除したい要素以外の要素で新しい配列を作成する方法もあります。それは次のようになります:
export const App: FC = () => {
  const [memos, setMemos] = useState<string[]>(["aaa", "bbb"]);

  const onMemoDelete = (i: number) => {
    setMemos(memos.filter((_, index) => index !== i));
  };
};
ここでは、`filter` メソッドを使用して新しい配列を作成します。この配列は、削除したい要素を除くすべての要素から構成されます。
このスクラップは2023/06/11にクローズされました