次世代のReact? Solid.jsについて


最初に

Solid.jsとは

Solid.jsとはReactに大きく影響を受けたライブラリであり比較的新しいライブラリです。私が興味を持ったのも2021年のState of JSを見て、Solid.jsの満足度が以下の画像のようにReactやSvelteよりも上位であること似驚き調べたからで、それまでは存在すら知りませんでした。

この記事の目的

Solidについて興味を持ったため簡単にSolidについて調査した結果を書いていこうと思います。また、Solidの認知度が少しでも上がり、開発手段の一つとして名前が上がるようになればと思っています。

SolidとReactの差

今回は主な違いでけを取り上げます。細かく知りたい方は公式サイトをご確認ください。

仮想DOM

まず大きな差が仮想DOMを使っているのかどうかです。Reactで仮想DOMを使っているのは有名で、仮想DOMのおかげで高速なSPA(シングルページアプリケーション)を作成できているのだろうと思っている方も非常に多いのではないでしょうか。私もその一人でした。Solidでは、この仮想DOMを使用せずにDOMを直接制御することで、さらに高速なSPAの作成が可能となっています。

パフォーマンス

Reactなどの主要なライブラリとのパフォーマンスの差が公式サイトにグラフで表示されており、以下がその画像です。
なんと、バニラとほぼ遜色の無いパフォーマンスを誇っています。
Reactで開発を行うとuseCallbackやuseMemorizeなどパフォーマンスを考慮する時間に開発時間の多くを投じているの人は少なくないのではないでしょうか。Solidでももちろんパフォーマンスを考慮する必要はありますが、少なからず開発者の負担は減るのではと考えています。

useStateとcreateSignal

以下はSolidでカウンターを作成した際のコードです。Reactとほとんど同じで、Reactを普段書いている方は問題なくコードが読めるのではないでしょうか。Reactとの大きな違いはuseStateかcreateSignalかの違いです。定義の仕方はuseStateと全く同じですが、countを使用するときにcount()となっています。これが大きな違いで、countはstateを返すgetterであり、そうすることで、あらゆる場所での変更を検知しています。

import { render } from "solid-js/web";
import { createSignal } from "solid-js";

function Counter() {
  const [count, setCount] = createSignal(0);
  const increment = () => setCount(count() + 1);
  
  return (
    <button type="button" onClick={increment}>
      {count()}
    </button>
  );
}

render(() => <Counter />, document.getElementById("app"));

useEffectとcreateEffect

以下のコードは、上記のコードにcreateEffectを追加したコードです。Reactエンジニアの方は違和感を持つのではないでしょうか。createEffectに第2引数がないのです。これがuseEffectとの大きな違いです。createSignalのところでも説明しましたが、countがgetterとなっていることで、createEffectで第2引数を指定しなくても、countの値が変わるたびにconsoleにcountの値が表示されます。是非このページでご確認ください。

import { render } from "solid-js/web";
import { createSignal, createEffect } from "solid-js";

function Counter() {
  const [count, setCount] = createSignal(0);
  const increment = () => setCount(count() + 1);

  createEffect(() => {
    console.log('count: ', count())
  })

  return (
    <button type="button" onClick={increment}>
      {count()}
    </button>
  );
}

render(() => <Counter />, document.getElementById("app"));

実際に実装してみる

ReactとSolidで簡単なTodoアプリを作成してみました。まずは、Reactです。今回は、SolidがViteを使用していることから、ReactでもViteを使用してアプリを作成しました。コードの全貌はgithubを確認してください。以下が主なコード部分です。

import { useRef, useState } from "react";
import "./App.css";

type Todo = {
  id: number;
  text: string;
  completed: boolean;
};

const App = () => {
  const [todos, setTodos] = useState<Todo[]>([]);
  let input = useRef<HTMLInputElement>(null);

  const addTodo = (text: string) => {
    setTodos([...todos, { id: ++todos.length, text, completed: false }]);
  };

  const toggleTodo = (id: number) => {
    console.log(id);
    setTodos(
      todos.map((todo) =>
        todo.id !== id ? todo : { ...todo, completed: !todo.completed }
      )
    );
  };

  return (
    <>
      <Header todoCount={todos.length} />
      <div>
        <input ref={input} />
        <button
          onClick={(e) => {
            if (!input.current?.value.trim()) return;
            addTodo(input.current.value);
            input.current.value = "";
          }}
        >
          Add Todo
        </button>
      </div>
      {todos.map((todo) => {
        const { id, text } = todo;
        console.log(`Creating ${text}`);
        return (
          <div key={id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(Number(id))}
            />
            <span
              style={{
                textDecoration: todo.completed ? "line-through" : "none",
              }}
            >
              {text}
            </span>
          </div>
        );
      })}
    </>
  );
};

const Header = ({ todoCount }: { todoCount: number }) => {
  return (
    <div
      style={{
        display: "flex",
        flexDirection: "row",
        alignItems: "center",
        justifyItems: "center",
      }}
    >
      <h1>Todo List</h1>
      <p
        style={{
          height: "12px",
          lineHeight: "12px",
          textAlign: "center",
          marginLeft: "20px",
        }}
      >
        todoCount: {todoCount}
      </p>
    </div>
  );
};
export default App;

inputからテキストを受け取りTodoを作成し、完了したかどうかをcheckboxで操作できます。また、ヘッダーで今のタスクの数を確認することが出来ます。Reactエンジニアの方はすんなり読めるのではと思います。

次に、Solidで書いたものです。こちらもコードの全貌はgithubでご確認ください。

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

type Todo = {
  id: number;
  text: string;
  completed: () => boolean;
  setCompleted: (completed: boolean) => void;
};

const App = () => {
  const [todos, setTodos] = createSignal<Todo[]>([]);
  let input: HTMLInputElement | undefined;
  let todoId = 0;

  const addTodo = (text: string) => {
    const [completed, setCompleted] = createSignal<boolean>(false);
    setTodos([...todos(), { id: ++todoId, text, completed, setCompleted }]);
  };
  const toggleTodo = (id: number) => {
    const index = todos().findIndex((t) => t.id === id);
    const todo = todos()[index];
    if (todo) todo.setCompleted(!todo.completed());
  };

  return (
    <>
      <Header todoCount={todos().length} />
      <div>
        <input ref={input} />
        <button
          onClick={(e) => {
            if (!input?.value.trim()) return;
            addTodo(input.value);
            input.value = "";
          }}
        >
          Add Todo
        </button>
      </div>
      <For each={todos()}>
        {(todo) => {
          const { id, text } = todo;
          console.log(`Creating ${text}`);
          return (
            <div>
              <input
                type="checkbox"
                checked={todo.completed()}
                onchange={[toggleTodo, id]}
              />
              <span
                style={{
                  "text-decoration": todo.completed() ? "line-through" : "none",
                }}
              >
                {text}
              </span>
            </div>
          );
        }}
      </For>
    </>
  );
};

const Header = ({ todoCount }: { todoCount: number }) => {
  return (
    <div
      style={{
        display: "flex",
        "flex-direction": "row",
        "align-items": "center",
        "justify-items": "center",
      }}
    >
      <h1>Todo List</h1>
      <p
        style={{
          height: "12px",
          "line-height": "12px",
          "text-align": "center",
          "margin-left": "20px",
        }}
      >
        todoCount: {todoCount}
      </p>
    </div>
  );
};
export default App;

いくつか疑問に思う書き方があるのではと思いますが、ここではcreateSignalについてのみ説明させていただきます。ReactのHooksは、関数コンポーネントの直下でしか使用できないという制限があります。しかし、SolidのcreateSignalは、どこでも使用できます。よって、以下のようなコードの書き方が可能になります。

  const addTodo = (text: string) => {
    const [completed, setCompleted] = createSignal<boolean>(false);
    setTodos([...todos(), { id: ++todoId, text, completed, setCompleted }]);
  };
  const toggleTodo = (id: number) => {
    const index = todos().findIndex((t) => t.id === id);
    const todo = todos()[index];
    if (todo) todo.setCompleted(!todo.completed());
  };

addTodoの関数の中でcreteSignalを使用して、getterとsetter両方をTodoに含めています。この書き方をすることによってどのようなメリットがあるでしょうか。それは、再レンダリングを防ぐ効果があります。Reactで書いた場合、Todoを作成しTodoのcheckBoxを押すとTodoにリスト全てが再生成されます。

しかし、同じように書いてもSolidではリスト全体の再レンダリングは起きません。

また、createSignalはグローバルに宣言することも出来ます。Reactでは、HeaderにTodoの数を渡すには、propsとして渡すことが一番簡単な方法ですが、もしHeaderまでの階層が深かったりすると、propsの橋渡しが大変です。それを解決するため、Redux, Recoil, Context APIなどが、あります。Solidでは、createSignalをグローバルに宣言することも可能なため、以下のように簡単に値を渡すことができます。

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

type Todo = {
  id: number;
  text: string;
  completed: () => boolean;
  setCompleted: (completed: boolean) => void;
};

const [todos, setTodos] = createSignal<Todo[]>([]);

const App = () => {
  let input: HTMLInputElement | undefined;
  let todoId = 0;

  const addTodo = (text: string) => {
    const [completed, setCompleted] = createSignal<boolean>(false);
    setTodos([...todos(), { id: ++todoId, text, completed, setCompleted }]);
  };
  const toggleTodo = (id: number) => {
    console.log(id);

    const index = todos().findIndex((t) => t.id === id);
    const todo = todos()[index];
    if (todo) todo.setCompleted(!todo.completed());
  };

  return (
    <>
      <Header />
      <div>
        <input ref={input} />
        <button
          onClick={(e) => {
            if (!input?.value.trim()) return;
            addTodo(input.value);
            input.value = "";
          }}
        >
          Add Todo
        </button>
      </div>
      <For each={todos()}>
        {(todo) => {
          const { id, text } = todo;
          console.log(`Creating ${text}`);
          return (
            <div>
              <input
                type="checkbox"
                checked={todo.completed()}
                onchange={[toggleTodo, id]}
              />
              <span
                style={{
                  "text-decoration": todo.completed() ? "line-through" : "none",
                }}
              >
                {text}
              </span>
            </div>
          );
        }}
      </For>
    </>
  );
};

const Header = () => {
  return (
    <div
      style={{
        display: "flex",
        "flex-direction": "row",
        "align-items": "center",
        "justify-items": "center",
      }}
    >
      <h1>Todo List</h1>
      <p
        style={{
          height: "12px",
          "line-height": "12px",
          "text-align": "center",
          "margin-left": "20px",
        }}
      >
        todoCount: {todos().length}
      </p>
    </div>
  );
};
export default App;

まとめ

今回は、SolidとReactの比較をしていきました。現状では、Reactの膨大なエコシステムが使えるという面でReactでの開発がメインとなりますが、そこが解決していくと、SolidがReactの代わりとなり、SPAを作るライブラリの代表となる未来が来るかもしれません。今回の記事では、最低限の説明しかしていないので、気になった部分や解説が足りない部分などあれば、コメントお願いします。時間があるときに追記します。また、現役のフロントエンドエンジニアの方がSolidについてどう思うのか、Solidの将来性、Solidのデメリットなどの感想などがありましたら、非常に興味があるのでコメントをしてくれると嬉しいです。

Solidは次世代のReactとなるのか