🐷

SolidJS触ってみた

2024/09/24に公開

対象読者

  • Reactから別のフロントエンドフレームワークへの移行を考えている人
  • SolidJSがなんとなく気になっている人

はじめに

社内でReactの乗り換え先としてSolidJSが話題になったので、触ってみました。
そこで、Reactと比較しながら、SolidJSのチュートリアルを進めた結果をまとめてみました。

SolidJSとは

Solid is a modern JavaScript framework designed to build responsive and high-performing user interfaces (UI).

SolidJSとは、レスポンシブでパフォーマンスの高いUIを構築するため設計された、JavaScriptのフレームワークです。それもどうやら仮想DOMを使っていないらしいです!

Solid Docs

チュートリアル

Docsには日本語版があり、実際に手を動かしながらチュートリアルを進めることができます。

SolidJS

以下では、チュートリアルに紹介されている機能の中で、特によく使いそうなものやReactとの比較ができそうなものを中心にピックアップしていきます。

createSignal

  • Reactの useState のような役割
  • useState の戻り値はstate変数とsetter関数の配列であるのに対して、createSignal の戻り値はgetterとsetterの2つの関数を持つ配列
  • Signalという要素をgetterで読み込み、setterで書き込んでいる
// SolidJS
// この例ではtextがgetterでsetTextがsetterになる
const [text, setText] = createSignal<string>("Hello World!");

// textはgetterなので、呼び出す際はtext()とする必要がある
console.log("text: ", text());

return <div>Text: {text()}</div>;

// React
const [text, setText] = useState<string>("Hello World!");

// textは変数なので、そのまま呼び出せる
console.log("text: ", text);

return <div>Text: {text}</div>;

createEffect

  • Reactの useEffect のような役割
  • useEffect では第2引数に依存値の配列を指定する必要があるのに対し、createEffect では依存関係を自動で読み込むので、依存値の配列を指定する必要がない
// SolidJS
createEffect(() => {
  console.log("text: ", text());
});

// React
useEffect(() => {
  console.log("text: ", text);
}, [text]);

onMount

  • Reactの useEffect の依存配列を空にした時のような役割
  • 最初のレンダリングが全て完了した後、コンポーネントに対して一度だけ実行される
// SolidJS
onMount(() => {
  console.log("mounted!");
});

// React
useEffect(() => {
  console.log("mounted!");
}, []);

createMemo

  • Reactの useMemo のような役割
  • Effectと同じく、 useMemo では第2引数に依存値の配列を指定する必要があるのに対し、createMemo では依存関係を自動で読み込むので、依存値の配列を指定する必要がない
// SolidJS
const doubleCount = createMemo(() => count() * 2);

// React
const doubleCount = useMemo(() => count * 2, [count]);

Show

  • Reactの三項演算子でレンダリングするような役割
  • when に渡された条件がtrueの場合に children が表示され、falseの場合に fallback が表示される
// SolidJS
<Show when={bool()} fallback={<p>false</p>}>
  <p>true</p>
</Show>

// React
{bool ? <p>true</p> : <p>false</p>}

For

  • Reactの .map でレンダリングするような役割
  • indexは定数ではなく、Signalであるため、呼び出す際は index() とする
// SolidJS
<For each={todos()}>
  {(todo, index) => (
    <div>
      {index()}: {todo.text}
    </div>
  )}
</For>

// React
{todos.map((todo, index) => (
  <div key={todo.id}>
    {index}: {todo.text}
  </div>
))}

Index

  • 使い方は For とほとんど同じ
  • Index ではindexが定数となり、todoがSignalであるため、呼び出す際は todo() とする
<Index each={todos}>
  {(todo, index) => (
    <div>
      {index}: {todo().text}
    </div>
  )}
</Index>

ForとIndexの使い分け

  • プリミティブ(文字列や数値など)を扱う時は Index を使用することで、再レンダリングの発生を抑制できる
const [todos, setTodos] = createSignal<{ id: number, text: string }[]>([]);
const handleEdit = (id: number, text: string) => {
  setTodos(todos().map((todo) => (todo.id === id ? { ...todo, text } : todo)));
};

// For
// textを更新するたびに再レンダリングが発生する
<For each={todos()}>
  {(todo) => {
    console.log(`For rendered : ${todo.text}`);
    return (
      <div>
        <input
          type="text"
          value={todo.text}
          onInput={(e) => handleEdit(todo.id, e.target.value)}
        />
      </div>
    );
  }}
</For>

// Index
// textを更新しても再レンダリングは発生しない(変更は反映される)
<Index each={todos()}>
  {(todo) => {
    console.log(`Index rendered: ${todo().text}`);
    return (
      <div>
        <input
          type="text"
          value={todo().text}
          onInput={(e) => handleEdit(todo().id, e.target.value)}
        />
      </div>
    );
  }}
</Index>

Switch

  • switch文のように、条件に応じてコンポーネントを表示する
// SolidJS
<Switch fallback={<Default />}>
  <Match when={status() === "loading"}>
    <Loading />
  </Match>
  <Match when={status() === "error"}>
    <Error />
  </Match>
  <Match when={status() === "success"}>
    <Success />
  </Match>
</Switch>

// React
const Status = (status: "loading" | "error" | "success" | "default") => {
  switch (status) {
    case "loading":
      return <Loading />;
    case "error":
      return <Error />;
    case "success":
      return <Success />;
    default:
      return <Default />;
  }
};

{Status(status)}

Dynamic

  • ShowSwitch コンポーネントを置き換えることで短い記述で完結できる
// Switch
<Switch fallback={<Default />}>
  <Match when={status() === "loading"}>
    <Loading />
  </Match>
  <Match when={status() === "error"}>
    <Error />
  </Match>
  <Match when={status() === "success"}>
    <Success />
  </Match>
</Switch>

// Dynamicで置き換えるとこうなる
const statuses = {
  loading: Loading,
  error: Error,
  success: Success,
  default: Default,
};

<Dynamic component={statuses[status()]} />

class

  • HTMLと同じclass属性でクラスを指定する
// SolidJS
<p class="active">active</p>

// React
<p className="active">active</p>

classList

  • 条件付きでクラスを簡潔に指定できる
// SolidJS
<h1 classList={{ active: count() > 0 }}>Counter: {count()}</h1>

// classで書くとこうなる
<h1 class={count() > 0 ? "active" : ""}>Counter: {count()}</h1>

// React
<h1 className={count > 0 ? "active" : ""}>Counter: {count}</h1>

ref

  • Reactの useRef のような役割
  • 変数を宣言してそのまま使える
// SolidJS
let containerRef;

<div ref={containerRef}>
	...
</div>

// コールバック関数の形にもできる
<div ref={(el) => console.log(el)}>
	...
</div>

// React
const containerRef = useRef<HTMLDivElement>(null);

<div ref={containerRef}>
	...
</div>

createStore

  • 使い方は createSignal とほとんど同じ
  • createStore で作成したオブジェクトを更新する際に produce を使うと、ピンポイントで変更を反映でき、再レンダリングを抑制できる
  • ForとIndexの使い分けで紹介したコードを createStoreproduce を使って書き換えてみると、For を使っても Index を使っても、textを更新した際の再レンダリングが発生しなくなる
  • 配列やオブジェクトを扱う際は基本的に createStore を使うのが良さそう
const [todos, setTodos] = createStore<{ id: number, text: string }[]>([]);
const handleEdit = (id: number, text: string) => {
  setTodos(
    (todo) => todo.id === id,
    produce((todo) => (todo.text = text))
  );
};

// For
// textを更新しても再レンダリングは発生しない(変更は反映される)
<For each={todos}>
  {(todo) => {
    console.log(`For rendered : ${todo.text}`);
    return (
      <div>
        <input
          type="text"
          value={todo.text}
          onInput={(e) => handleEdit(todo.id, e.target.value)}
        />
      </div>
    );
  }}
</For>

// Index
// textを更新しても再レンダリングは発生しない(変更は反映される)
<Index each={todos}>
  {(todo) => {
    console.log(`Index rendered: ${todo().text}`);
    return (
      <div>
        <input
          type="text"
          value={todo().text}
          onInput={(e) => handleEdit(todo().id, e.target.value)}
        />
      </div>
    );
  }}
</Index>

さいごに

チュートリアルやドキュメントを通して感じたSolidJSの優れた点と課題となる点をReactと比較しながらまとめていきます。

優れた点

  • シンプルに書ける

    • Reactの乗り換え先としてはかなり良さそう
  • 再レンダリングを抑制・低減するための仕組みがある

    • 仮想DOMを使わないことによって実現しているらしい
      • こちらに関しては話題としてかなり重くなるため、今回は深掘りしません
    • createStore + produce でのオブジェクトや配列の更新が便利そう(シンプルに書けるのも良い)
  • 高パフォーマンス

    • VanillaJSに次ぐ速さ

    出典: https://www.solidjs.com/

    出典: https://www.solidjs.com/

課題となる点

  • コミュニティが小さく、エコシステムや日本語の情報が充実していない
    • コミュニティの規模
      • State of JavaScript 2023 によると、SolidJSを「使ったことがある人」は、21,432人中9%で、「使ったことはないが聞いたことがある人」は、67.7%
        • Reactを「使ったことがある人」は、21,619人中84.4%で、「使ったことはないが聞いたことがある人」は、15.5%
    • 公式が紹介しているエコシステム
      • 200件に満たないが、最低限のものは揃っていそう
      • 大規模なプロジェクトになると、辛い部分がでてきそう
      • 特にcss・UI フレームワークとの統合で問題になりそう
    • 日本語の情報
株式会社AVILEN

Discussion