ReactにおけるES6ジェネレーターを使ったステート管理

JavaScriptのステート管理について話すとき、通常はreduxやReactのsetStateのようなフレームワーク固有のライブラリーやツールを使用することについて言及します。しかし、私たちがステート管理戦略を探しているとき、ES6には私たちの手元に強力なツールがあると思います。もし私たちが逐次的なステート更新(マルチステップフォーム、カルーセル、アニメーションなど)を実装している場合、そのようなツールの一つがジェネレーターです。

ジェネレーターとは何か?

ジェネレーターは、イテレータープロトコルを実装する特別なJavaScript関数です。もし以前に使ったことがなければ、初めてそれを理解しようとする時には少し学習曲線が必要ですので頑張ってください!

もし既にイテレーターについて知っているなら、次に進んで構いませんが、そうでない場合は、10秒で説明しましょう。

イテレーター入門

ある関数がイテレータープロトコルを実装するためには、next関数を含んだオブジェクトを返す必要があります。このnext関数はvaluedone属性を持ったオブジェクトを返します。

const myIterator = createIterator();
const { value, done } = myIterator.next();

自分自身のイテレーター関数をこのように作ることができます。

function createColorIterator() {
  let i = 0;
  const colors = ["red", "yellow", "blue"];
  return {
    next: () => {
      if (i < colors.length) {
        let value = colors[i];
        i++;
        return {
          value: value,
          done: false
        };
      } else {
        return {
          value: undefined,
          done: true
        };
      }
    }
  };
}

let iterator = createColorIterator();

console.log(iterator.next());
// { value: "red", done: false }
console.log(iterator.next());
// { value: "yellow", done: false }
console.log(iterator.next());
// { value: "blue", done: false }
console.log(iterator.next());
// { value: undefined, done: true }

このイテレーターは、createColorIterator関数のローカルスコープを使って自身の状態を追跡します。この場合、iの値を変更することは、イテレーター内で維持されます。さらなる情報については、ここでクロージャーやレキシカルスコーピングについて読むことができます。

また、JSの任意のイテラブル(配列、文字列、マップ、セットなど)は、イテレーターを返すSymbol.iteratorというプロパティがあります。

const colors = ["red", "yellow", "blue"];
const iterator = colors[Symbol.iterator]();

console.log(iterator.next());
// { value: "red", done: false }
// ...上と同じ

ジェネレーターに戻りましょう...

イテレーターは素晴らしいです!しかし、ゼロからイテレーターを構築することは、多くの定型文の記述を意味することがあります。ここでジェネレーターが登場します!ジェネレーターは、イテレーターを作成するためのES6の魔法のような特別な関数です。ジェネレーターは非同期プログラミングに非常に役立ちますが、ここではその話はしません。

たとえば、function*の構文を使用して、より少ないコードで私のイテレーターを書き直すことができます。

function* createColorIterator() {
  let i = 0;
  const colors = ["red", "yellow", "blue"];
  while (i < colors.length) {
    const color = colors[i];
    i++;
    yield color;
  }
}

console.log(iterator.next());
// { value: "red", done: false }
// ...上と同じ

yieldキーワードを使用しているこのジェネレーター関数に注目してください。ジェネレーターはこのキーワードに遭遇すると、直ちに関数から抜け出しyieldの後にある値を返します。その後、nextが再び呼び出されると関数の実行は再開されます。

Reactアプリケーションにおいて、ジェネレーターをステートを保存するためにどのように使用できるか?

ジェネレーターによって、私たちには多くの可能性が広がります!現状、この単純な色の例を続けましょう。

Reactコンポーネントでは、ジェネレーターの現在の状態を保存するための1つの状態変数を作る必要があります。これは主に、現在のイテレーター状態に基づいてビューコンポーネントをレンダリングしているたびに、この状態が更新された際に再レンダリングを引き起こすためです。

let colors = createColorIterator();
let initialState = colors.next();

function App() {
  const [colorState, setColorState] = React.useState(initialState);

  function updateBackgroundColor() {
    setColorState(colors.next());
  }

  function reset() {
    colors = createColorIterator();
    setColorState(initialState);
  }

  const { value, done } = colorState;

  return (
    <Container backgroundColor={value}>
      <h1>Hello!</h1>
      <Button disabled={done} onClick={updateBackgroundColor}>
        Change background color
      </Button>
      <Button onClick={reset}>Reset</Button>
    </Container>
  );
}

私がイテレーターや初期状態の値をコンポーネントのスコープの外で定義していることに注目してください。これは、再レンダリングするたびにこの状態をリセットすることを避けるためです。

色のジェネレーター例 - CodeSandbox

なぜステート管理のためにジェネレーターを使用するか?

ほとんどのユースケースでは、ジェネレーターをもっと伝統的なステート管理戦略よりも使用することをお勧めしません。ジェネレーターは純粋な関数ではありません(同じ引数で呼ばれてもその値が変わるため)、私たちはreduxやuseReducerのようなより関数型のステート管理戦略とともにジェネレーターを使用することはできません。

しかしながら、インクリメンタルなステート更新の作成、コンポーネントレンダリングから独立した状態のテスト(しかし、コンポーネントの統合テストも)、そしてフレームワーク間でのコード共有のために、ここにはたくさんの可能性があると思います。この記事のためにはしていませんが、AngularやVueで同じロジックを実装することは、核となるロジックを変更することなく、かなり簡単にできるでしょう。

こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/beccaliz/es6-generators-for-state-management-in-react-h7b