反応を処理する技術(immerを用いると不変性を維持しやすい)


前章では,素子更新性能を最適化する方法と,不変のまま更新状態を維持することが重要である理由を学習した.展開演算子と配列の組み込み関数を使用すると、配列またはオブジェクトを簡単にコピーし、新しい値を上書きできます.ただし、オブジェクトの構造が非常に深くなると、不変性を維持しながら更新することは非常に困難になります.
const object = {
  somewhere: {
    deep: {
      inside: 3,
      array: [1, 2, 3, 4],
    },
    bar: 2,
  },
  foo: 1,
};

// somewhere.deep.inside 값을 4로 바꾸기
let nextObject = {
  ...object,
  somewhere: {
    ...object.somewhere,
    deep: {
      ...object.somewhere.deep,
      inside: 4,
    },
  },
};

// somewhere.deep.array에 5 추가하기
let nextObject2 = {
  ...object,
  somewhere: {
    ...object.somewhere.deep,
    array: object.somewhere.deep.array.concat(5),
  },
};
値を更新するには、10行のコードを記述する必要があります.このように展開演算子は、既存の値を保持しながら必要な値を再指定するためによく使用されます.
実際のプロジェクトでもこのような複雑な状況に遭遇することがあります.毎回展開演算子を複数回使用するのは面倒な作業です.可読性もよくない.
この場合、immerというライブラリを使用すると、複雑な構造のオブジェクトも、不変性を維持しながら更新するために短いコードを容易に購入することができます.
実習プロセス.
  • immerをインストールし、使用状況を確認します.
  • immerを使用して単純プロジェクト
  • を作成

    12-1. immerをインストールし、その使い方を理解します。


    12-1-1. プロジェクトの準備


    immerの使用方法を熟知するためにプロジェクトを作成します.
    yarn create react-app immer-tutorial
    cd immer-tutorial
    yarn add immer

    12-1-2. immerを使用せずに不変性を維持


    まず、immerを使用せずに不変性の更新値を維持する構成部品を作成してみます.App.jsでは以下のように記述する.
    import React, { useRef, useCallback, useState } from "react";
    
    const App = () => {
      const nextId = useRef(1);
      const [form, setForm] = useState({ name: "", username: "" });
      const [data, setData] = useState({
        array: [],
        uselessValue: null,
      });
    
      // input 수정을 위한 함수
      const onChange = useCallback(
        (e) => {
          const { name, value } = e.target;
          setForm({
            ...form,
            [name]: [value],
          });
        },
        [form]
      );
    
      // form 등록을 위한 함수
      const onSubmit = useCallback(
        (e) => {
          e.preventDefault();
          const info = {
            id: nextId.current,
            name: form.name,
            username: form.username,
          };
    
          // array에 새 항목 등록
          setData({
            ...data,
            array: data.array.concat(info),
          });
    
          // form 초기화
          setForm({
            name: "",
            username: "",
          });
          nextId.current += 1;
        },
        [data, form.name, form.username]
      );
    
      // 항목을 삭제하는 함수
      const onRemove = useCallback(
        (id) => {
          setData({
            ...data,
            array: data.array.filter((info) => info.id !== id),
          });
        },
        [data]
      );
    
      return (
        <div>
          <form onSubmit={onSubmit}>
            <input
              name="username"
              placeholder="아이디"
              value={form.username}
              onChange={onChange}
            />
            <input
              name="name"
              placeholder="이름"
              value={form.name}
              onChange={onChange}
            />
            <button type="submit">등록</button>
          </form>
          <div>
            <ul>
              {data.array.map((info) => (
                <li key={info.id} onClick={() => onRemove(info.id)}>
                  {info.username} ({info.name})
                </li>
              ))}
            </ul>
          </div>
        </div>
      );
    };
    
    export default App;
    実行時

    簡単な構成部品が作成され、フォームにユーザー名/名前を入力するとドロップダウン・リストに追加され、リスト・アイテムをクリックすると削除されます.このように展開演算子と配列内蔵関数を使用して不変性を維持するのは難しくありませんが、状態が複雑になると少し面倒になる可能性があります.

    12-1-3. immerの使い方


    immerを使用すると、不変性を維持する作業を非常に簡単に処理できます.使用方法:
    import produce from 'immer'
    const nextState = produce((originalState, draft) => {
      // 바꾸고 싶은 값 바꾸기
      draft.somewhere.deep.inside = 5
    })
    productという関数は2つのパラメータを受け入れます.1番目のパラメータは変更したいステータスで、2番目のパラメータはステータスを更新する方法を定義する関数です.
    2番目のパラメータに渡されたni関数の内部で必要な値を変更すると、product関数は不変性を維持する代わりに新しい状態を生成します.
    このライブラリの核心は「不変性を考慮しないようにコードを記述するが、不変性管理をしっかりと行う」ことです.奥まった値を簡単に変えるだけでなく、並べ替えの処理も容易で便利だという.
    次に、より複雑なデータを一定に維持して更新するコードの例を示します.
    import produce from "immer";
    
    const originalState = [
      {
        id: 1,
        todo: `전개 연산자와 배열 내장 함수로 불변성 유지하기`,
        checked: true,
      },
      {
        id: 2,
        todo: `immer로 불변성 유지하기`,
        checked: false,
      },
    ];
    
    const nextState = produce((originalState, draft) => {
      // id가 2인 항목의 checked 값을 true로 설정
      const todo = draft.find((t) => t.id === 2); // id로 항목 찾기
      todo.checked = true;
      // 혹은 draft[1].checked = true
    
      // 배열에 새로운 데이터 추가
      draft.push({
        id: 3,
        todo: `일정 관리 앱에 immer 적용하기`,
        checked: false,
      });
    
      // id = 1인 항목을 제거하기
      draft.splice(
        draft.findIndex((t) => t.id === 1),
        1
      );
    });

    12-1-4. immerをApp構成部品に適用する


    作成したばかりのAppコンポーネントにimmerを適用し、より簡潔なコード更新状態にします.
    import React, { useRef, useCallback, useState } from "react";
    import produce from "immer";
    
    const App = () => {
      const nextId = useRef(1);
      const [form, setForm] = useState({ name: "", username: "" });
      const [data, setData] = useState({
        array: [],
        uselessValue: null,
      });
    
      // input 수정을 위한 함수
      const onChange = useCallback(
        (e) => {
          const { name, value } = e.target;
          setForm(
            produce((form, draft) => {
              draft[name] = value;
            })
          );
        },
        [form]
      );
    
      // form 등록을 위한 함수
      const onSubmit = useCallback(
        (e) => {
          e.preventDefault();
          const info = {
            id: nextId.current,
            name: form.name,
            username: form.username,
          };
    
          // array에 새 항목 등록
          setData(
            produce((data, draft) => {
              draft.array.push(info);
            })
          );
    
          // form 초기화
          setForm({
            name: "",
            username: "",
          });
          nextId.current += 1;
        },
        [data, form.name, form.username]
      );
    
      // 항목을 삭제하는 함수
      const onRemove = useCallback(
        (id) => {
          setData(
            produce((data, draft) => {
              draft.array.splice(
                draft.array.findIndex((info) => info.id === id),
                1
              );
            })
          );
        },
        [data]
      );
    
      return (
        <div>
          <form onSubmit={onSubmit}>
            <input
              name="username"
              placeholder="아이디"
              value={form.username}
              onChange={onChange}
            />
            <input
              name="name"
              placeholder="이름"
              value={form.name}
              onChange={onChange}
            />
            <button type="submit">등록</button>
          </form>
          <div>
            <ul>
              {data.array.map((info) => (
                <li key={info.id} onClick={() => onRemove(info.id)}>
                  {info.username} ({info.name})
                </li>
              ))}
            </ul>
          </div>
        </div>
      );
    };
    
    export default App;
    immerを使用して構成部品ステータスを作成する場合は、オブジェクトの値を直接変更するか、push、spliceなどの関数を使用して配列を直接変更できます.したがって、不変性を維持することに慣れていないが、JavaScriptに慣れている場合は、構成部品の状態に必要な変化を簡単に反映できます.immerを使用すると、コードが簡潔になるとは限らない.onRemoveの場合、配列内蔵関数フィルタを使用するコードはより簡潔であるため、immerを強制的に使用する必要はありません.immerは不変性を保つコードが複雑な場合にのみ使用すれば十分である.

    12-1-5. userStateの関数式更新をimmerとともに書き込む


    前章では、useStateの関数式更新について説明します.
    import { useCallback } from "react"
    
    const [number, setNumber] = useState(0)
    // prevNumbers는 현재 number 값을 가리킨다.
    const onIncrease = useCallback(
      () => setNumber(prevNumber => prevNumber + 1),
      []
    )
    immerが提供するproduct関数を呼び出すと、最初のパラメータが関数形式の場合、更新関数が返されます.
    import produce from "immer";
    
    const update = produce(draft => {
      draft.value = 2
    })
    
    const originalState = {
      value: 1,
      foo: 'bar'
    }
    
    const nextState = update(originalState)
    console.log(nextState) // { value: 2, foo: 'bar'}
    これらのインスタントメッセージのプロパティをuseStateの関数式更新と組み合わせて使用すると、コードをよりきれいにすることができます.
    import React, { useRef, useCallback, useState } from "react";
    import produce from "immer";
    
    const App = () => {
      const nextId = useRef(1);
      const [form, setForm] = useState({ name: "", username: "" });
      const [data, setData] = useState({
        array: [],
        uselessValue: null,
      });
    
      // input 수정을 위한 함수
      const onChange = useCallback((e) => {
        const { name, value } = e.target;
        setForm(
          produce((draft) => {
            draft[name] = value;
          })
        );
      }, []);
    
      // form 등록을 위한 함수
      const onSubmit = useCallback(
        (e) => {
          e.preventDefault();
          const info = {
            id: nextId.current,
            name: form.name,
            username: form.username,
          };
    
          // array에 새 항목 등록
          setData(
            produce((draft) => {
              draft.array.push(info);
            })
          );
    
          // form 초기화
          setForm({
            name: "",
            username: "",
          });
          nextId.current += 1;
        },
        [form.name, form.username]
      );
    
      // 항목을 삭제하는 함수
      const onRemove = useCallback((id) => {
        setData(
          produce((draft) => {
            draft.array.splice(
              draft.array.findIndex((info) => info.id === id),
              1
            );
          })
        );
      }, []);
    
      return (
        <div>
          <form onSubmit={onSubmit}>
            <input
              name="username"
              placeholder="아이디"
              value={form.username}
              onChange={onChange}
            />
            <input
              name="name"
              placeholder="이름"
              value={form.name}
              onChange={onChange}
            />
            <button type="submit">등록</button>
          </form>
          <div>
            <ul>
              {data.array.map((info) => (
                <li key={info.id} onClick={() => onRemove(info.id)}>
                  {info.username} ({info.name})
                </li>
              ))}
            </ul>
          </div>
        </div>
      );
    };
    
    export default App;
    Product関数のパラメータを関数形式として使用すると,コードがより簡潔になる.