Open12

JavaScript調べ物メモ

y_meganey_megane

日付のあれこれ
toISOString

toJSONString, toISOString以外はブラウザ依存。
JSON.stringfy
toISOStringを使う。Dateの関数はISOStringの書式をパースできる。

y_meganey_megane

setStateしてstateは更新されてるけどDOM再描画されない。
DebugツールのHooks>Stateも更新されない…?
ソース適当に触って強制的に再描画させると反映される。Why...

import React, { FC, useState } from 'react';

interface Family {
  name: string;
  age: string;
}

const MutableList: FC = () => {
  const [families, setFamilies] = useState<Family[]>([]);

  const handleAdd = () => {
    const len = families.length;
    const newFamilies = families;
    newFamilies.push({ name: 'xxx', age: len.toString() });
    console.log(newFamilies.length);
    setFamilies(newFamilies);
  };

  return (
    <div>
      <li>
        {families.map((x, index) => (
          <div key={`key_${index.toString()}`}>
            <ul>
              <span>{x.name}</span>
              <span>{x.age}</span>
            </ul>
          </div>
        ))}
      </li>
      <button type="button" onClick={handleAdd}>
        Add
      </button>
    </div>
  );
};
export default MutableList;
y_meganey_megane
    setFamilies([{ name: 'xxx', age: '20' }]);

べた書きしたら反映されたのでsetFamiliesに渡してる値が悪いのか。

    const newFamily = { name: 'xxx', age: 10 };
    setFamilies([...families, newFamily]);

これで解決した。

const newFamilies = families;
newFamilies.push({ name: 'xxx', age: len.toString() });
setFamilies(newFamilies);

この書き方だと結局stateとして持ってるFamily配列にpushしてるのと同じ?
stateの値自体は更新されてるけどsetStateによるDOM再描画が走ってない。
setStateの仕組みがわかってないな。

y_meganey_megane

Reactがrenderの内容を更新してくれなくてハマった - アルゴリズムとかオーダーとか

一つはまりやすい点としてsetStateの引数に渡す値が配列やオブジェクトの場合、中身だけ変更して次回もそのまま渡すと、配列やオブジェクトそのものは同一と判定されてしまい、更新チェックがされずレンダリングが発生しません。

というのがそれっぽいんだけど、ドキュメント見てもそれっぽいこと見つけられない…
...で展開して追加して、要するに新規オブジェクトとして作って与えればいいのは分かったのだけど、なぜ。
公式だと

useState は自動的な更新オブジェクトのマージを行いません。この動作は関数型の更新形式をスプレッド構文と併用することで再現可能です

とあって、置換しちゃうから追記したければ...で展開して追加しろとあるけど、同一オブジェクトで置換すること自体には言及してない気がするが、そういうものなのか…

y_meganey_megane

Hooksのソース追った記事
React HooksのuseStateがどういう原理で実現されてるのかさっぱりわからなかったので調べてみた

これを手がかりにjsもReactも怪しいけど、更新処理であるupdateReducer読んでみた。
https://github.com/facebook/react/blob/c21c41ecfad46de0a718d059374e48d13cf08ced/packages/react-reconciler/src/ReactFiberHooks.js

        // Mark that the fiber performed work, but only if the new state is
        // different from the current state.
        if (newState !== hook.memoizedState) {
          markWorkInProgressReceivedUpdate();
        }

ここ(newState !== hook.memoizedState)で前回のオブジェクトと比較して、不一致のときのみ更新ありとみなすのかな。

y_meganey_megane
  const handleAdd = () => {
    // setStateの更新用関数は同一オブジェクトだと差分なしを見なす。
    // スプレッド演算子やコピーなどで新規オブジェクトとして渡す必要がある。
    // ドキュメントでは単に「マージではなく置換」とあるが、同一オブジェクトだとDOM再描画されない。
    // ※Stateの値自体は更新されるので、何かしら別の要因で再描画されると反映される。
    setFamily([...families, { name: '', age: 0 }]);
  };
y_meganey_megane

フォームの入力行(名前、年齢)を追加可能にしたフォーム
コンポーネント化すべきとか色々あるけど、フォームとstate制御以外の要素を省くためべた書き。

import React, { FC, useState } from 'react';

interface Family {
  name: string;
  age: string;
}

const MyForm: FC = () => {
  const [families, setFamily] = useState<Family[]>([]);

  const handleSubmit = (e: React.MouseEvent) => {
    e.preventDefault();
    alert(JSON.stringify(families));
  };

  // フォーム入力イベントハンドラ;
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    // nameに連番を付与して、handlerで文字列的に取得してフォーム行数とstateの配列番号を対応付ける
    const props = e.target.name.split('__');
    const name = props[0];
    // eslint-disable-next-line no-eval
    const index: number = eval(props[1]);

    // もっとスマートな書き方がある気がする...
    const newFamilies = [...families];
    const family = newFamilies[index];
    const newFamily = { ...family, [name]: e.target.value };
    newFamilies[index] = newFamily;
    setFamily(newFamilies);
  };

  // フォーム行追加ハンドラ
  const handleAdd = () => {
    setFamily([...families, { name: '', age: '' }]);
  };

  return (
    <div>
      <form>
        <table>
          <thead>
            <tr>
              <td>Name</td>
              <td>Age</td>
            </tr>
          </thead>
          <tbody>
            {families.map((x, index) => (
              <tr key={`key_${index.toString()}`}>
                <td>
                  <input
                    name={`name__${index.toString()}`}
                    type="text"
                    value={x.name}
                    onChange={handleChange}
                  />
                </td>
                <td>
                  <input
                    name={`age__${index.toString()}`}
                    type="number"
                    value={x.age}
                    onChange={handleChange}
                  />
                </td>
              </tr>
            ))}
            <tr>
              <td>
                <button type="button" onClick={handleAdd}>
                  Add
                </button>
              </td>
              <td>
                <button type="button" onClick={handleSubmit}>
                  Send
                </button>
              </td>
            </tr>
          </tbody>
        </table>
      </form>
    </div>
  );
};
export default MyForm;