tiiti
@tiiti

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

Reactのstateについて教えていただきたいです。

stateがどのような動きをしているのか教えていただきたいです。

reactにて簡単なTodoアプリを作成しています。
その中で試行錯誤するうちに記述したコードがなぜその挙動をするか掴めなくなってしまい、
ご質問させていただきました。

オープンな質問になってしまい申し訳ありませんが、
アドバイスを頂戴できますと幸いです。

// import React from "react";
import React, { useState } from "react";
import { v4 as uuidv4 } from "uuid";

const App = () => {
  const [todoText, setTodoText] = useState("");

  const [addTodoList, setAddTodoList] = useState([]);

  const handleChange = (e) => setTodoText(e.target.value);
 
  const onClickAdd = () => {
    if (todoText === "") return;
    setAddTodoList([...addTodoList, { comment: todoText, status: "作業中" }]);
    setTodoText("");
  };

  const [deleteTodoList, setDeleteTodoList] = useState([]);
  const clickDeleteButton = (todo, index) => {
    setDeleteTodoList(addTodoList.splice(index, 1));
    console.log("addTodoList", addTodoList);
    console.log("todo", todo);
    console.log("index", index);
    console.log(deleteTodoList);
  };

  const [completeTodos, setCompleteTodos] = useState([]);
  const clickStatusButton = (todo, index) => {
    if (todo.status === "作業中") {
      setCompleteTodos(() => {
        todo.status = "完了";
      });
    }
    console.log("addTodoList", addTodoList);
    console.log("completeTodos", completeTodos);
    console.log(index, todo);
  };

  return (
    <div>
      <h1>TodoList</h1>
      <input type="radio" />
      すべて
      <input type="radio" />
      作業中
      <input type="radio" />
      完了
      <table>
        <thead>
          <tr>
            <td>ID</td>
            <td>コメント</td>
            <td>状態</td>
          </tr>
        </thead>
        <tbody>
          {addTodoList.map((todo, index) => (
            <tr key={uuidv4()}>
              <td>{`${index + 1}`}</td>
              <td>{`${todo.comment}`}</td>
              <td>
                <button
                  onClick={() => clickStatusButton(todo, index)}
                >{`${todo.status}`}</button>
              </td>
              <td>
                <button onClick={() => clickDeleteButton(todo, index)}>
                  {/* addTodoListのtodoとindexを渡している */}
                  {/* イベントハンドラで引数がある場合()=>関数(引数)の記載が必要 */}
                  削除
                </button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
      <h2>新規タスクの追加</h2>
      <input type="text" value={todoText} onChange={handleChange} />
      <button onClick={() => onClickAdd()}>追加</button>
    </div>
  );
};

export default App;

タスクのstatusを変更する下記についてです。

const [completeTodos, setCompleteTodos] = useState([]);
  const clickStatusButton = (todo, index) => {
    if (todo.status === "作業中") {
      setCompleteTodos(() => {
        todo.status = "完了";
      });
    }
    console.log("addTodoList", addTodoList);
    console.log("completeTodos", completeTodos);
    console.log(index, todo);
  };

上記の挙動としては、
→タスクを複数入力
→一回目、「作業中」ボタンをクリックすると「完了」ボタンに変化
logを確認するとstatusも完了に「変化」している
→二回目、他のタスクの「作業中」ボタンをクリック、ボタンは「作業中」のまま。
logを確認するとタスクのstatusは「完了」に変化している。

また、setStateの認識として、下記をイメージしております。
const [completeTodos, setCompleteTodos] = useState([]);
[変数,変更するための関数]=初期値
setCompleteTodos(completeTodosをどうするか)

この通りに当てはめていくと、上記で記載したコードでは、
初期値空の配列、「completeTodos」のtodo.statusを完了にする、という事になるのかなと思いました。
また、statusを完了にして「completeTodos」の中に格納するのかともちらっと思いましたが、ログを確認するとそのうな形式は特にありませんでした。
正直よく分からないけど、「addTodoList」の中のtodo.statusは変更できる、一回目だけボタンの表示も変わる、、なぜこの挙動をするか分からなくなってしまいました。
そもそもsetStateの認識が間違っているのかなと思いましたが、決定的な理解につながらずご質問させていただきました。

1

4Answer

const onClickAdd = () => {
  if (todoText === "") return;

  // 関数型の更新
  // https://ja.reactjs.org/docs/hooks-reference.html#functional-updates
  setAddTodoList((previous) => {
    return [...previous, { id: uuidv4(), comment: todoText, status: "作業中" }]
  });
  setTodoText("");
};

関数型の更新を使ったほうが無難です。こうすることでaddTodoListが最新のものかどうか注意する必要がない。

addTodoListが常に更新されない関数内とかにある場合(例:useEffect useMemo ..)、古いデータをもとに更新してしまう可能性が残る

更にid: uuidv4()としておけば、

{addTodoList.map((todo, index) => (
            <tr key={uuidv4()}>

とすることなく

{addTodoList.map((todo, index) => (
            <tr key={todo.id}>

と出来るし、こうしないと常に各要素のkeyが毎度新しいので(uuidv4())結果は多分同じですが、毎度無駄に描画されることになる。

1Like

Comments

  1. @tiiti

    Questioner

    コメントありがとうございます!!
    参考にさせていただきます。

回答ではなくて恐縮なのですが、私もReact始めたばかりなのでこの点に興味があります。

自分で調べたところでは、このあたりがヒントになりそうです。
試したところ、自分のサンプルコードは正しく動いているようです。

解説できるほど深掘りできてはいないので、参考までに・・・
有識者からの回答をお待ちしてます。

state を直接変更しないこと

変更するのではなく置き換える

However, although objects in React state are technically mutable, you should treat them as if they were immutable—like numbers, booleans, and strings. Instead of mutating them, you should always replace them.

0Like

Comments

  1. @tiiti

    Questioner

    コメントありがとうございます。
    また、URLも添付してくださりありがとうございます。
    参考にさせていただきます。

setStateに渡すコールバックはなんでもいいわけではなく,直前のstateを引数に取り,新しいstateを返すものでなくてはいけません.
setStateの反映は非同期のため,配列など前状態の損失が許されない場合にコールバックを渡します.

既に指摘されている通り,stateの内容を直接書き換えることはReactライフサイクルに対する並行変更にあたり,やってはいけない行為になります.
変更したいstateオブジェクトがある場合は,必ず新しいオブジェクトを渡すようにしてください.

1回だけ作業中ボタンが反応するのは,completeToDosが[]からundefinedになるためです.何故そうなるのかはsetCompleteToDosを使う辺りのコードをよく見るとわかると思います.
というかまあ,todosに完了状態を保持するなら,completeTodosは要らない子になりますね.

0Like

Comments

  1. @tiiti

    Questioner

    いつもご返信いただきありがとうございます、、
    参考にさせていただきます!!

Your answer might help someone💌