🧊

Solid/Svelte/(P)Reactで、それぞれリストを描画するコードの比較

お題

  • 配列に詰まったアイテムをそれぞれ表示する
  • アイテムを表示するコンポーネントには副作用がある
    • つまり、不要なre-renderを避けたい
    • 今回は単に初期表示時点の`Date.now()`
  • アイテムは増減する

という、あるあるな要件を、それぞれのフレームワークだとどう書くことになるか。

端的には、アイテムを増減させても、関係のない各アイテムのコンポーネントはそっとしておいてほしい。

Solid

import { For, createSignal } from "solid-js";

const Item = (props) => (
  <div>
    #{props.item} at {Date.now()}
    <button onClick={props.delete}>DEL</button>
  </div>
);

const App = () => {
  const [items, setItems] = createSignal([1]);
  const addItem = () => setItems((prev) => [...prev, prev.length + 1]);
  const deleteItem = (idx) =>
    setItems((prev) => [...prev.filter((_, i) => idx !== i)]);

  return (
    <>
      <button onClick={addItem}>ADD</button>
      <hr />
      
      <For each={items()}>
        {(item, idx) => (
          <Item item={item} delete={() => deleteItem(idx())} />
        )}
      </For>
    </>
  );
};

`For`コンポーネントでループを表現するだけで、不要なre-renderがスキップされるところがポイント。

Svelte

<!-- ================== item.svelte ===================== -->
<script>
  export let item;
</script>

<div>
  #{item} at {Date.now()}
  <button on:click>DEL</button>
</div>

<!-- ================== app.svelte ===================== -->
<script>
  import Item from "./item.svelte";

  let items = [1];
  const addItem = () => {
    items = [...items, items.length + 1];
  };
  const deleteItem = (idx) => {
    items = [...items.filter((_, i) => idx !== i)];
  };
</script>

<button on:click={addItem}>ADD</button>
<hr>

{#each items as item, idx}
  <Item {item} on:click={() => deleteItem(idx)} />
{/each}

こちらも`#each`で書けばre-renderされない。`on:click`って書くだけでイベントがdelegateできるのも嬉しい。

(P)React

import { useState, memo } from "react";

const Item = memo(
  ({ item, delete: del }) => (
    <div>
      #{item} at {Date.now()}
      <button onClick={del}>DEL</button>
    </div>
  ),
  (a, b) => a.item === b.item
);

const App = () => {
  const [items, setItems] = useState([1]);
  const addItem = () => setItems((prev) => [...prev, prev.length + 1]);
  const deleteItem = (idx) => 
    setItems((prev) => [...prev.filter((_, i) => idx !== i)]);

  return (
    <>
      <button onClick={addItem}>ADD</button>
      <hr />
      {items.map((item, idx) => (
        <Item item={item} delete={() => deleteItem(idx)} key={item} />
      ))}
    </>
  );
};

`memo()`でラップして、どういう条件ならre-renderしなくていいかを指定するのがポイント。

まとめ

  • Solid
    • JSXなので、`map()`で書くこともでき(その場合はre-renderしちゃう)、そういうところは中途半端
    • 必要なところだけ使えばいい、選べて嬉しいという見方もできる
  • Svelte
    • ループを表現するには`#each`しかなく、それで最適化がなされるのでわかりやすい
    • 手数が最も少なくて素敵だが、コンポーネントはかさばる
  • (P)React
    • 良くも悪くも何もしてくれない
    • そういうのは全部手動でやらないといけないので、もっと条件が複雑になったら?みたいな話になる
    • Reactの場合は別途`react-dom`税がかかる

Vue派の人はすいません。けどたぶん`v-for`を使ったときはよしなにしてくれるので、Solidに近い体験な気がする。