122
78

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

次のプロジェクトSolidJSで作りませんか?

Last updated at Posted at 2023-06-22

はじめに

最適なフレームワークを選択できていますか?

WEBアプリケーションを開発する際、現在Reactがデファクトスタンダードになりつつあるのは事実です。
その理由は、Reactが持つ柔軟性や豊富なエコシステムから来る信頼性などが上げられます。
しかし、どんなユースケースにおいても必ずReactが最適な選択とは限りません。
プロジェクトの目的に応じて都度最適なフレームワークを選択する必要があると考えます。

SolidJS

公式サイト
本記事では、WEBアプリを開発する際の新しい選択肢としてSolidJSというフレームワークを紹介します。
SolidJSはReactのような宣言的なUIライブラリでありながら、Reactにはない良さを持っています。

SolidJSの特徴

SolidJSの主な特徴は以下の通りです。

  • Reactのように記述できる
    SolidJSは基本的にReactの哲学を持っています。
    JSXを使ってコンポーネントを作り、それを積み上げることでアプリケーションを構築します。
    Reactなどのフレームワーク経験者にとっては学習コストが低く、すぐに開発を始めることができます。

  • パフォーマンス重視
    Reactの仮想DOMとは違い、SolidJSでは状態/プロパティの更新を直接DOMに反映することで、きめ細かいリアクティビティを実現しています。
    これにより高頻度に更新があるようなアプリケーションにおいても高いパフォーマンを維持することができます。
    SolidJSはUIスピードとメモリ消費において、VanillaのJSとほぼ同等のパフォーマンスを発揮します。(公式調べ)
    またバンドルサイズも非常に小さく、アプリケーションのロード時間を短縮することが可能です。

  • シンプルなAPI
    SolidJSは比較的シンプルなAPIで構成されており、学習コストが低く始められます。
    主要な機能を押さえておけばシンプルなアプリケーションを簡単に構築することができます。

ReactユーザーこそSolidJSを使うべき理由

Reactを使用している開発者にとって、SolidJSへの切り替えは、いくつかの有利な点があります。

  • Reactと記述が似ている
    前述したとおり、SolidJSはReactの哲学を受け継いで作られています。
    Reactにある便利な機能の多くがSolidJSに標準装備されているため、Reactからの移行が非常にスムーズに行えます。

  • Reactよりも効率的に記述ができる
    Reactのようにmemo化による効率化が不要だったり、データバインディングが簡単に記述できるなど、Reactのかゆいところにも手が届くような作りになっています。
    SolidJSはReactよりも少ないコードでよりパフォーマンスの良いアプリケーションを開発することができます。

Reactから受け継いでいる良い点

Reactが世界中の開発者から広く支持されている理由は、開発体験の素晴らしさにあります。
SolidJSは、これらの価値ある特性を尊重し、自身の設計に取り入れています。

JSXでの記述

JSXは、JavaScriptのシンタックスを拡張した言語で、XML構文のようにコンポーネントを記述することが可能です。
SolidJSでもReactと同様に、JSXを用いて簡潔なコンポーネントの定義が可能です。

import { render } from 'solid-js/web';

function App() {
  return <h1>Hello, Solid!</h1>;
}

render(App, document.getElementById('root'));

TypeScriptのサポート

TypeScriptは、JavaScriptに静的型付けとオブジェクト指向プログラミングの特性を付加した言語で、コードの安全性と生産性を向上させます。
SolidJSもTypeScriptをフルにサポートしており、安全に型を使用しながら開発を行うことが可能です。

import { render } from 'solid-js/web';

interface GreetingProps {
  name: string;
}

function Greeting({ name }: GreetingProps) {
  return <h1>Hello, {name}!</h1>;
}

render(() => <Greeting name="Solid" />, document.getElementById('root'));

lazy関数

SolidJSはReactのReact.lazy()関数の機能を継承し、遅延ロードやコード分割をサポートしています。
これにより、アプリケーションの初回ロードパフォーマンスを向上させることが可能です。

import { lazy } from 'solid-js';

const LazyComponent = lazy(() => import('./LazyComponent'));

function App() {
  return (
    <div>
      <LazyComponent />
    </div>
  );
}

Portalの利用

ReactのPortalと同じ概念がSolidJSにもあります。
これにより、DOMのツリー構造を超えて子コンポーネントをレンダリングすることが可能となります。
これは、モーダルダイアログなどの特定のコンポーネントを実装する際に非常に便利です。

import { createSignal } from 'solid-js';
import { Portal } from 'solid-js/web';

function App() {
  const [isOpen, setIsOpen] = createSignal(false);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen())}>Toggle Modal</button>
      {isOpen() && (
        <Portal mount={document.body}>
          <div>I am a modal!</div>
        </Portal>
      )}
    </div>
  );
}

ErrorBoundaryとSuspense

ReactのErrorBoundaryやSuspenseのようなメカニズムもSolidJSに存在します。
これらを活用することで、エラーハンドリングや非同期処理の待機をうまく管理することができます。

import { createResource } from 'solid-js';
import { ErrorBoundary, Suspense } from 'solid-js/web';

function App() {
  const [resource] = createResource(fetchData);

  return (
    <ErrorBoundary fallback={<div>Something went wrong!</div>}>
      <Suspense fallback={<div>Loading...</div>}>
        <Component resource={resource} />
      </Suspense>
    </ErrorBoundary>
  );
}

ref属性

SolidJSでは、Reactのref属性と同じように、DOMノードやコンポーネントのインスタンスを参照することができます。
これは、フォームの入力値を取得したり、外部ライブラリとの連携を行う際に非常に便利です。

import { createSignal } from 'solid-js';

function App() {
  const [inputRef, setInputRef] = createSignal(null);

  return (
    <div>
      <input ref={setInputRef} />
      <button onClick={() => console.log(inputRef().value)}>Log Input Value</button>
    </div>
  );
}

Reactよりも良い点

Reactから受け継いだ優れた特性と機能に加えて、SolidJSにはReactを上回る強みも多く存在します。

useEffectの第二引数が不要

ReactのuseEffectでは依存性配列(第二引数)を必要としますが、SolidJSではその必要がありません。
これはSolidJSがリアクティブな依存性を自動的に追跡してくれます。

import { createSignal, createEffect, onCleanup } from 'solid-js';

function App() {
  const [count, setCount] = createSignal(0);

  createEffect(() => {
    console.log(count());
  });

  return <div>Count: {count()}</div>;
}

state/recoilの思想が標準搭載

SolidJSでは、ReactのuseStateやrecoilのような状態管理の仕組みが標準搭載されています。
createSignalはReactでいうところのuseStateと同じようなものですが、コンポーネントの外でも使用することができます。
これにより、recoilのようなグローバルな状態管理を簡単に実現することが可能です。
recoilのようなアトミックな状態管理も、SolidJSのリアルDOMによって実現されています。

import { createSignal } from 'solid-js';

// globalなステート
const [globalCount, setGlobalCount] = createSignal(0);

function App() {
  // localなステート
  const [count, setCount] = createSignal(0);

  return (
    <div>
      <button onClick={() => setCount(count() + 1)}>Increase</button>
      <p>Count: {count()}</p>
      <p>Global Count: {globalCount()}</p>
    </div>
  );
}

classList

SolidJSでは、要素のクラスを動的に操作するためにclassListディレクティブを提供しています。
これにより、コンポーネントのスタイルを状態に基づいて動的に変更することが可能です。

import { createSignal } from 'solid-js';

function App() {
  const [isActive, setIsActive] = createSignal(false);

  return (
    <div
      classList={{
        active: isActive(),
      }}
    >
      I am {isActive() ? 'active' : 'not active'}
    </div>
  );
}

カスタムディレクティブ

SolidJSでは、カスタムディレクティブを定義することができます。
これにより、共通の動作を再利用したり、DOMの状態をステートに結びつけたりすることが可能です。

const [name, setName] = createSignal("");

function model(el, value) {
  const [field, setField] = value();
  createRenderEffect(() => (el.value = field()));
  el.addEventListener("input", (e) => setField(e.target.value));
}

<input type="text" use:model={[name, setName]} />;

SolidJSの注意点、React経験者がハマりやすいポイント

SolidJSが提供する優れた特性と機能は魅力的ですが、その一方で注意すべき点やReactユーザーがハマりやすいポイントが存在します。

Signal

SolidJSでは、リアクティブな値の生成や管理に、Signalという概念が使用されます。
このSignalは、状態を保持するための関数で、読み取りと書き込みが可能です。
ただしその振る舞いはReactのuseStateとは若干異なり、これに慣れるまで少し時間がかかるかもしれません。

きちんと理解するには、SolidJSのリアクティビティの仕組みについて理解する必要があります。

Reactでは、ある状態が更新されるとコンポーネントのrender関数が再実行され、DOMが再レンダリングされます。
再レンダリングの度に仮想DOMが生成され、前回の状態と比較することで差分を検出し、差分のみを実際のDOMに反映しています。

一方SolidJSでは、ある状態が更新されると、その状態に依存するDOM(正確にはDOMを生成する関数)や反応関数(createEffect等)のみが再実行されます。
これにより、同じコンポーネントないの無関係なDOMは影響をうけず、関係するDOMのみが更新されれます。

例えば次のようなコンポーネントがあったときに、button要素をクリックすることでcountの値が更新されるが、
countに依存している反応関数と、p要素のtextContentだけが再実行/再作成されて、無関係のDOMは影響を受けません。

import { createSignal, createEffect } from 'solid-js';

function App() {
  const [count, setCount] = createSignal(0);

  // 反応関数
  createEffect(() => {
    console.log(count());
  });

  return (
    <div>
      <button onClick={() => setCount((prevCount) => prevCount + 1)}>Increase</button>
      <p>Count: {count()}</p>
      <div>無関係なDOM</div>
    </div>
  );
}

コードだけみると、useStateと同じように見えますが、実際には異なる振る舞いをしています。
Reactユーザーがやりがちな間違いとして、次のようなコードがあります。

import { createSignal } from 'solid-js';

function App() {
  const [count, setCount] = createSignal(0);
  
  // ここ
  const color = count() > 0 ? 'red' : 'blue';

  return (
    <div>
      <button onClick={() => setCount((prevCount) => prevCount + 1)}>Increase</button>
      <p style={{ color: color }}>Count: {count()}</p>
    </div>
  );
}

このコードでは一見、colorという変数はcountの値に応じて色を設定しているように見えますが、
この記述方法ではcolorという変数はcountの依存関係にあるという風にSolidJSは解釈してくれません。
そのため、countの値が更新されてもcolorの値は更新されず、colorの値に応じてp要素の色も更新されません。

このような場合は、colorを関数でラップする必要があります。(もしくはcreateMemoを利用する)
これにより、colorcountの値に応じて更新されるようになり、p要素もcolorの値に応じてstyleが変化します。

import { createSignal } from 'solid-js';

function App() {
  const [count, setCount] = createSignal(0);

  const color = () => count() > 0 ? 'red' : 'blue';

  return (
    <div>
      <button onClick={() => setCount((prevCount) => prevCount + 1)}>Increase</button>
      <p style={{ color: color() }}>Count: {count()}</p>
    </div>
  );
}

Propsの扱い

SolidJSでは、親コンポーネントから子コンポーネントへの値の受け渡し(props)は、Reactと基本的に同じ方法で行われます。
ただし、関数コンポーネントにおけるpropsは、それ自体がリアクティブな値として振る舞います。
そのため、先ほどのSignalと同様に、中身の値を取り出す際は注意が必要です。

import { createSignal } from 'solid-js';

// ng
// countがリアクティブにならない
function Child(props) {
    const { count } = props

  return <p>Count: {count}</p>;
}

// ok
// propsのまま使う
function Child(props) {
  return <p>Count: {props.count}</p>;
}

// ok
// 関数でラップする
function Child(props) {
    const count = () => props.count

  return <p>Count: {count()}</p>;
}

function App() {
  const [count, setCount] = createSignal(0);

  return (
    <div>
      <button onClick={() => setCount((prevCount) => prevCount + 1)}>Increase</button>
      <Child count={count()} />
    </div>
  );
}

制御フロー(Show/Switch/For/Index)

SolidJSではリアクティブな値を扱うため、制御フローの記述方法がReactとは異なります。

Show

Reactでは、条件によって表示する要素を切り替える場合、三項演算子を利用することが多いと思います。

function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      {count > 0 ? <p>Count: {count}</p> : <p>Count is 0</p>}
      <button onClick={() => setCount((prevCount) => prevCount + 1)}>Increase</button>
    </div>
  );
}

SolidJSでは、Showコンポーネントを利用します。

import { Show } from 'solid-js';

function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <Show when={count() > 0}>
        <p>Count: {count()}</p>
      </Show>
      <Show when={count() === 0}>
        <p>Count is 0</p>
      </Show>
      <button onClick={() => setCount((prevCount) => prevCount + 1)}>Increase</button>
    </div>
  );
}

Switch

Switchを利用することで、より複雑な条件分岐を記述することができます。

import { Switch, Match } from 'solid-js';

function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <Switch fallback={<p>Count is {count()}</p>}>
        <Match when={count() === 0}>
          <p>Count is 0</p>
        </Match>
        <Match when={count() === 1}>
          <p>Count is 1</p>
        </Match>
      </Switch>
      <button onClick={() => setCount((prevCount) => prevCount + 1)}>Increase</button>
    </div>
  );
}

For

Reactでは、配列の要素を展開する場合、map関数を利用することが多いと思います。

function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      {Array.from({ length: count }, (_, i) => (
        <p key={i}>Count: {i}</p>
      ))}
      <button onClick={() => setCount((prevCount) => prevCount + 1)}>Increase</button>
    </div>
  );
}

SolidJSではForまたはIndexを使い、配列の要素を展開します。

import { For } from 'solid-js';

function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <For each={Array.from({ length: count })}>
        {(_, i) => (
          <p key={i}>Count: {i}</p>
        )}
      </For>
      <button onClick={() => setCount((prevCount) => prevCount + 1)}>Increase</button>
    </div>
  );
}

Getting Started

これまでの内容で、SolidJSがReactユーザーに慣れ親しんだ方法で記述できることが分かったかと思います。
またReactユーザーがハマりやすいポイントも押さえたところで、実際にSolidJSを利用してみましょう。

こちらを参考に進めます。

プロジェクトの作成

まずは、SolidJSのプロジェクトを作成します。
TypeScriptを利用して開発することを推奨します。

npx degit solidjs/templates/ts my-app # my-appという名前でプロジェクトを作成
cd my-app # my-appに移動
npm i # 依存関係のインストール
npm run dev # ブラウザが立ち上がる

ファイル構成

SolidJSのプロジェクトは、Reactのプロジェクトとほぼ同じ構成で開発ができます。
Reactにも流派があるように、正解はないですが私がよく採用する構成を紹介します。

my-app
├── package.json
├── src
│   ├── assets
│   │   └── favicon.svg
│   ├── components/ # コンポーネント
│   ├── utils/ # ユーティリティ
│   ├── pages/ # ページ
│   ├── App.tsx
│   ├── index.css
│   └── index.tsx
├── index.html
├── tsconfig.json
└── vite.config.ts

ルーティング

App.tsxにルーティングを記述します。
ルーティングには@solidjs/routerというライブラリを利用します。
次のように記述することで、ルーティングを実現できます。

import { Route, Routes } from '@solidjs/router';
import { Component, lazy } from 'solid-js';

const App: Component = () => {
    return (
        <Routes>
            <Route path="/" component={lazy(() => import('./pages/Home'))} />
        </Routes>
    );
};

export default App;

チュートリアル・サンプルコード

チュートリアルやサンプルコードは次を参照すると良いです。

https://www.solidjs.com/tutorial/introduction_basics
https://www.solidjs.com/examples/counter

SolidJSのユースケース

SolidJSは、Reactと比較してまだまだ利用者が少なく、Reactの大きなコミュニティと豊富なエコシステムと比較するとデメリットが多いです。
一方で、SolidJSがパフォーマンスを発揮するようなユースケースも存在します。

高頻度で状態が更新されるようなアプリケーション

例えばマウスの座標を状態に記録し、その座標を扱うようなアプリケーションがある場合、
ReactだとuseStateを利用して状態を管理することになります。
Reactでは、状態が更新されると再レンダリングが走るので、ミリ秒単位で再レンダリングが行われることになります。
これだけだとそこまで影響はないように見えますが、アプリケーションが複雑化するにつれて再レンダリングのコストが上がっていき、画面がカクつくようになります。

import { useState } from 'react';

function App() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  return (
    <div>
      <p>Position: {position.x}, {position.y}</p>
      <div onMouseMove={(e) => setPosition({ x: e.clientX, y: e.clientY })}>
        <p>Move here</p>
      </div>
    </div>
  );
}

SolidJSでは、createSignalを利用して状態を管理することができます。
SolidJSでも同様にミリ秒単位で状態の更新が行われますが、再実行されるのはpositionに依存する関数のみなので影響範囲は最小限に押さえることができます。
Reactだとカクつきが気になるようなアプリケーションでも、SolidJSで実装することでカクつきが気にならなくなります。

import { createSignal } from 'solid-js';

function App() {
  const [position, setPosition] = createSignal({ x: 0, y: 0 });

  return (
    <div>
      <p>Position: {position().x}, {position().y}</p>
      <div onMouseMove={(e) => setPosition({ x: e.clientX, y: e.clientY })}>
        <p>Move here</p>
      </div>
    </div>
  );
}

フォームをよく使うアプリケーション

フォームをよく使うアプリケーションでは、フォームの入力値を状態として管理することになります。
Reactでフォームを使う場合、inputタグのvalue属性に状態をバインドすることでフォームの入力値を状態として管理することができます。
しかし、Reactでは状態が更新されると再レンダリングが走るので、フォームの入力値が更新されるたびに再レンダリングが走ってしまいます。
SolidJSでは状態が更新されても全体の再レンダリングは走らないので、スムーズなフォームを実装することができます。

それでもReactを採用するケース

中規模以上のアプリケーション

やはりReactの豊富なエコシステムはとても優秀です。
SolidJSはまだまだ利用者が少なく、エコシステムもまだまだ整っていません。
アプリケーションの規模が大きくなるにつれて、安定して動く周辺ライブラリがある方が便利です。
SolidJSでは周辺ライブラリの選択肢が限定的で、機能も不十分なことが多いです。
このように中規模以上のアプリケーションにおいてはReactを選択する方が無難な場合もあります。

React-to-SolidJS

それでもSolidJSを選択してくれる読者に、React脳からSolidJS脳になるためのTipsをいくつか紹介します。

useXX to createXX

ReactではuseXXという名前のフックを利用して状態を管理します。
SolidJSではcreateXXという名前の関数を利用して状態を管理します。
主要な対応表は以下です

useState/atom(recoil) = createSignal

useStateはローカルの状態、atomはグローバルの状態を管理するのに使うと思います。SolidJSではcreateSignalをローカル・グローバル問わず利用することができます。

useEffect = createEffect

状態の変更に応じて、何かを実行したい際はcreateEffectを利用します。
useEffectと違い、コンポーネントの最初に記述しないといけないというルールはありません。
コンポーネントの外でも定義することができます。
useEffectと違い第二引数も必要なく、自動的にどの状態に依存しているかを判断してくれます。
createMemoも同様にコンポーネントの外から利用可能で、第二引数も不要です。

styleはcamelCaseではなくkebab-case

// React
<div style={{ backgroundColor: 'red' }} />

// SolidJS
<div style={{ 'background-color': 'red' }} />

最適化のコツ・上級者になるには

SolidJSは極めようと思うとかなり奥が深いです。
最適化や上級者になるためのコツをほんの一部紹介します。

batchによる一括更新

ある処理の中で複数のSignalを更新する場合、それぞれのSignalの更新によって依存関係にあるDOMや反応関数が再実行されます。
これを一度の更新にまとめることで、再実行される回数を減らすことができます。

import { batch } from 'solid-js';

function App() {
  const [count, setCount] = createSignal(0);
  const [count2, setCount2] = createSignal(0);

  const sum = createMemo(() => count() + count2())

  const handleClick = () => {
    batch(() => {
      setCount(count() + 1);
      setCount2(count2() + 1);
    });
  };

  return (
    <div>
      <p>Sum: {sum()}</p>
      <button onClick={handleClick}>Click</button>
    </div>
  );
}

For/Indexの使い分け

配列をDOMに展開する方法として、ForIndexがあります。
正しく使い分けることがパフォーマンスに大きく影響します。
どちらを使うのが適切か、Reactユーザーにとっては非常に分かりやすい見分け方があります。
Reactで配列を操作する際は配列のmapメソッドを利用します。
展開した要素には必ずkeyを設定しないといけません。
このkeyに要素のキーとなるようなIDを使う場合は、Forを使ってください。
keyに要素のインデックスを使う場合は、Indexを使ってください。

// 配列のmapメソッドを利用する場合
const arr = ['a', 'b', 'c'];

// Reactでkeyに要素のキーとなるようなIDを使う場合
arr.map((item) => <div key={item}>{item}</div>);
// SolidJSではForを使う(indexがSignal)
<For each={arr}>{(item, index) => <div>{index()}. {item}</div>}</For>

// Reactでkeyに要素のインデックスを使う場合
arr.map((item, index) => <div key={index}>{item}</div>);
// SolidJSではIndexを使う(itemがSignal)
<For each={arr}>{(item, index) => <div>{index}. {item()}</div>}</For>

Forを使う場合にも注意点があります。
例えばユニークな文字列の配列をDOMに展開する場合は、そのままForを使うことで最適化されます。
例えばこの配列のSignalが更新された場合、更新された要素のみが再描画されます。

// ユニークな文字列の配列
const arr = ['a', 'b', 'c'];

// これは最適化される
<For each={arr}>{(item) => <div>{item}</div>}</For>

一方でオブジェクトの配列を扱う場合、Reactユーザーからすると違和感を覚える箇所があります。
SolidJSにはkeyを指定する場所がありません。
オブジェクトをForで展開する場合、そのオブジェクトが更新をされると配列全体が再描画されることになります。

これを防ぐために、オブジェクトの配列をForで扱う際には一度、キーとなるプロパティを使って文字列の配列に変換します。
そのうえで、キーとなるプロパティからオブジェクトを取得できるようなMapを作成します。
この二つを使い、Forで展開することで最適化されます。
少し回りくどいですが、この方法でないと配列全体が再描画されてしまうので注意してください。

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

interface Item {
  id: string;
  name: string;
}

// オブジェクトの配列
const [arr, setArr] = createSignal<Item[]>([
  { id: "a", name: "A" },
  { id: "b", name: "B" },
  { id: "c", name: "C" },
]);

// キーの配列に変換
const keys = createMemo(() => arr().map((item) => item.id));

// Mapを作成
const map = createMemo(() => {
  const map = new Map<string, Item>();
  arr().forEach((item) => map.set(item.id, item));
  return map;
});

function App() {
  return (
    <For each={keys()}>
      {(key) => {
        const item = () => map().get(key)!;
        return <div>{item().name}</div>;
      }}
    </For>
  );
}

最後に

SolidJSは完成度が高いですが、エコシステムやコミュニティはまだまだ発展途上のフレームワークです。
ユーザーが増えることでエコシステムも整っていくことが見込めます。
ぜひ一度SolidJSを触って、一緒にSolidJSを盛り上げましょう!

参考

122
78
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
122
78

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?