SolidJSに入門してみた2:ForとIndexの違い

2022.06.20

こんちには。

データアナリティクス事業本部機械学習チームの中村です。

SolidJSのチュートリアルやってみた記事その2です。今回は制御フローForIndexの違いについて書きます。

なお本記事は以下のチュートリアルをやってみて、ココは結構Reactと違うなとか、個人的にハマった部分などフォーカスしています。

詳しく知りたい方はぜひチュートリアルをやってみてください!

環境セットアップ

お手元で動かしたい場合は、以下でOKです。 Reactでもおなじみのグルグルするやつが起動します。

$ npx degit solidjs/templates/ts sample-solidjs
$ cd sample-solidjs
$ npm i
$ npm run dev

リストサンプルを例に

前回も使いましたが、リスト表示するコンポーネントを題材にします。

制御フローForを使用した場合は、以下でした。

import type { Component } from 'solid-js';
import { createSignal } from 'solid-js';
import { JSX } from 'solid-js';
import { For } from 'solid-js';

export const SampleList: Component = () => {
  const [cats, setCats] = createSignal([
    { id: 'J---aiyznGQ', name: 'Keyboard Cat' },
    { id: 'z_AbfPXTKms', name: 'Maru' },
    { id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' }
  ]);

  const changeListHandler: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> = () => {
    setCats([
        ...cats().slice(1, cats().length),
        cats()[0]
    ]);
  }

  return (
    <>
      <button onClick={changeListHandler}>Click Me</button>
      <ul>
        <For each={cats()}>{ (cat, i) => {
          console.log(`rendered ${i()}: ${cat.name}`);
          return (
            <li>
              {i()}: {cat.name}
            </li>
          );
        }}</For>
      </ul>
    </>
  );
};

Indexを使った場合

Forと似たものとしてIndexという制御フローもあります。これを使用して先ほどのサンプルを以下のように書き直すことができます。

import type { Component } from 'solid-js';
import { createSignal } from 'solid-js';
import { JSX } from 'solid-js';
import { Index } from 'solid-js';

export const SampleList: Component = () => {
  const [cats, setCats] = createSignal([
    { id: 'J---aiyznGQ', name: 'Keyboard Cat' },
    { id: 'z_AbfPXTKms', name: 'Maru' },
    { id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' }
  ]);

  const changeListHandler: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> = () => {
    setCats([
        ...cats().slice(1, cats().length),
        cats()[0]
    ]);
  }

  return (
    <>
      <button onClick={changeListHandler}>Click Me</button>
      <ul>
        <Index each={cats()}>{ (cat, i) => {
          console.log(`rendered ${i}: ${cat().name}`);
          return (
            <li>
              {i}: {cat().name}
            </li>
          );
        }}</Index>
      </ul>
    </>
  );
};

ほぼ違いはないのですが、Signalとなっているものが配列のインデックスiではなく要素catであるため、要素のアクセスにはcat()を使用しています。

ForとIndexの違い

再レンダリングが発生する条件が異なり、Indexの方がより再レンダリングが発生しにくくなっています。

  • For
    • 現在の配列と異なる要素が入ってくると再レンダリングが発生
    • 要素がプリミティブ(文字列・数値など)な場合、値が一致するなら異なる要素とはみなされない。
    • 要素がオブジェクトの場合、値が一致しても異なるオブジェクトであれば異なる要素とみなされる。
    • 異なる要素でない場合でも、要素数が増えた場合は再レンダリングが発生
  • Index
    • 要素数が増えた場合のみ再レンダリングが発生

確かめてみる

まずはForの例に戻り、ボタンを押すと新しいオブジェクトに置き換わる場合を見てみます。

import type { Component } from 'solid-js';
import { createSignal } from 'solid-js';
import { JSX } from 'solid-js';
import { For } from 'solid-js';

export const SampleList: Component = () => {
  const [cats, setCats] = createSignal([
    { id: 'J---aiyznGQ', name: 'Keyboard Cat' },
    { id: 'z_AbfPXTKms', name: 'Maru' },
    { id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' }
  ]);

  const changeListHandler: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> = () => {
    setCats([
        ...cats().slice(1, cats().length),
        { id: '1000', name: 'New Cat1'}
    ]);
  }

  return (
    <>
      <button onClick={changeListHandler}>Click Me</button>
      <ul>
        <For each={cats()}>{ (cat, i) => {
          console.log(`rendered ${i()}: ${cat.name}`);
          return (
            <li>
              {i()}: {cat.name}
            </li>
          );
        }}</For>
      </ul>
    </>
  );
};

上記の場合、ボタンを押す都度再レンダリングが発生します。これをIndexに置き換えると、

import type { Component } from 'solid-js';
import { createSignal } from 'solid-js';
import { JSX } from 'solid-js';
import { Index } from 'solid-js';

export const SampleList: Component = () => {
  const [cats, setCats] = createSignal([
    { id: 'J---aiyznGQ', name: 'Keyboard Cat' },
    { id: 'z_AbfPXTKms', name: 'Maru' },
    { id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' }
  ]);

  const changeListHandler: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> = () => {
    setCats([
        ...cats().slice(1, cats().length),
        { id: '1000', name: 'New Cat1'}
    ]);
  }

  return (
    <>
      <button onClick={changeListHandler}>Click Me</button>
      <ul>
        <Index each={cats()}>{ (cat, i) => {
          console.log(`rendered ${i}: ${cat().name}`);
          return (
            <li>
              {i}: {cat().name}
            </li>
          );
        }}</Index>
      </ul>
    </>
  );
};

ボタンを押しても再レンダリングが発生しないことが分かります。(ちなみに表示にはきちんと変更が反映されます)

逆にIndexのコールバック処理で以下のように文字列の連結などをしていると、

import type { Component } from 'solid-js';
import { createSignal } from 'solid-js';
import { JSX } from 'solid-js';
import { Index } from 'solid-js';

export const SampleList: Component = () => {
  const [cats, setCats] = createSignal([
    { id: 'J---aiyznGQ', name: 'Keyboard Cat' },
    { id: 'z_AbfPXTKms', name: 'Maru' },
    { id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' }
  ]);

  const changeListHandler: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> = () => {
    setCats([
        ...cats().slice(1, cats().length),
        { id: '1000', name: 'New Cat1'}
    ]);
  }

  return (
    <>
      <button onClick={changeListHandler}>Click Me</button>
      <ul>
        <Index each={cats()}>{ (cat, i) => {
          console.log(`rendered ${i}: ${cat().name}`);
          const name_with_id = `${cat().name} (id:${cat().id})`
          return (
            <li>
              {i}: {name_with_id}
            </li>
          );
        }}</Index>
      </ul>
    </>
  );
};

この場合、コールバックは実行されないため画面表示が更新されません。これをForで記述すると、

import type { Component } from 'solid-js';
import { createSignal } from 'solid-js';
import { JSX } from 'solid-js';
import { For } from 'solid-js';

export const SampleList: Component = () => {
  const [cats, setCats] = createSignal([
    { id: 'J---aiyznGQ', name: 'Keyboard Cat' },
    { id: 'z_AbfPXTKms', name: 'Maru' },
    { id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' }
  ]);

  const changeListHandler: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> = () => {
    setCats([
        ...cats().slice(1, cats().length),
        { id: '1000', name: 'New Cat1'}
    ]);
  }

  return (
    <>
      <button onClick={changeListHandler}>Click Me</button>
      <ul>
        <For each={cats()}>{ (cat, i) => {
          console.log(`rendered ${i()}: ${cat.name}`);
          const name_with_id = `${cat.name} (id:${cat.id})`
          return (
            <li>
              {i()}: {name_with_id}
            </li>
          );
        }}</For>
      </ul>
    </>
  );
};

表示がきちんと更新されることがわかります。

ただこの場合、コールバック内に関数を定義してあげればIndexのまま実装することも可能です。

import type { Component } from 'solid-js';
import { createSignal } from 'solid-js';
import { JSX } from 'solid-js';
import { Index } from 'solid-js';

export const SampleList: Component = () => {
  const [cats, setCats] = createSignal([
    { id: 'J---aiyznGQ', name: 'Keyboard Cat' },
    { id: 'z_AbfPXTKms', name: 'Maru' },
    { id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' }
  ]);

  const changeListHandler: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> = () => {
    setCats([
        ...cats().slice(1, cats().length),
        { id: '1000', name: 'New Cat1'}
    ]);
  }

  return (
    <>
      <button onClick={changeListHandler}>Click Me</button>
      <ul>
        <Index each={cats()}>{ (cat, i) => {
          console.log(`rendered ${i}: ${cat().name}`);
          const name_with_id = () => `${cat().name} (id:${cat().id})`
          return (
            <li>
              {i}: {name_with_id()}
            </li>
          );
        }}</Index>
      </ul>
    </>
  );
};

公式の説明によると

公式の説明では以下のように記載されています。

経験則では、プリミティブを扱うときには を使用します。

要は文字列・数値などのプリミティブな要素を持つ配列を扱う場合はIndexを使用し、そうでない場合はForを使うことを推奨されています。

オブジェクトの配列であってもIndexを使用することで、再レンダリングを抑制できますが、表示に影響がないか十分に注意して使用する必要がありそうです。逆によくわからない場合はForを使うことにより、表示上の問題が出にくくなるといえそうです。

まとめ

いかがでしたでしょうか?ForIndexの制御フローの違いがちょっと難しかったですので、サンプルを動かしながら再レンダリングの条件を色々調べて理解することができました。

この記事がSolidJSの理解の助けになりましたら幸いです。