SolidJSでTodoデスクトップアプリを作ってみてわかったこと

Daisuke.W

2022.06.15 フロントエンドLT会 - vol.7

自己紹介

Daisuke@React SolidJS大好き侍

(Twitter: @fullStackHaruno)

  • 医療機器メーカー電気電子系エンジニアとして従事
    • 回路設計やハーネス設計→ソフトウェア運用テスト設計&業務自動化アプリ開発
  • 現在はフロントエンドエンジニアを目指して日々アウトプット中!
  • TypeScript、ReactやSolidJSが大好き
  • LT登壇は2回目(フロントエンドLT会は初!!)

SolidJSでTodoデスクトップアプリを作ってみてわかったこと

キッカケ

ある日、Qiitaの記事 「React大好き侍が、「もうSolidJSでいいじゃん…//」ってなったワケ。」 を読んで興味が湧いた

公式チュートリアルをやってみた


書き方やコードの見た目はReactそっくりだが、完全に別物だった

  • 変化値の読み取りは関数を用いる
  • Cleanupが必要な関数の扱いがラク
  • propsの扱いは注意が必要
  • 制御フローのヘルパー関数のおかげでreturn内の見通しが良くなった
  • etc…

変化値の読み取りは関数を用いる


function App() {
  const [count, setCount] = createSignal(0);
  // countがゲッター(値の読み取り)
  // setCountがセッター(値の書き込み)

  return (
    <button onClick={() => setCount(count() + 1)}>
      Count: {count()}
    </button>
  );
}

動作デモ

return()内とcreateEffect()内のどこでゲッターが読みとられたのかを追跡して変更があれば更新する

Cleanupが必要な関数の扱いがラク


1秒ごとにカウントアップする例

const App = () =>{
  const [count, setCount] = createSignal(0);
  
  // 1秒ごとに加算
  const intervalID = setInterval(() => setCount(prev => prev + 1), 1000);
  // コンポーネントが破棄される時にクリーンアップを実行
  onCleanup(() => clearInterval(intervalID));
  
  return <div>Count: {count()}</div>;
}

SolidJSはコンポーネントを再レンダリングしないので、setIntervalは一度だけ呼び出される

動作デモ

Reactで同じ書き方をした場合、際限なくsetIntervalを呼び出して暴走する

propsの扱いは注意が必要


Counterコンポーネント側で分割代入をすると、countの追跡が途切れるため初期値0から変わらない

const Counter = ({ count }) => {
  return <div>Count: {count}</div>; // 0
};

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

  return (
    <>
      <Counter count={count()} />
      <button onClick={() => setCount((prev) => prev + 1)}>加算</button>
    </>
  );
};

動作デモ

SolidJSでのpropsの扱い方


分割代入せずにprops.countとすることで追跡できる

const Counter = (props) => {
  return <div>{props.count}</div>   // 0,1,2...
}

動作デモ

propsのデフォルト値はmergePropsで設定できる

const Counter = (props) => {
  const merged = mergeProps({ count: 0 }, props);
  return <div>{merged.count}</div>   // 0,1,2...
}

動作デモ

以下の定義方法はならpropsの個別設定ができる

const Counter = (props: { count }) => {...}

動作デモ

制御フローのヘルパー関数のおかげでreturn内の見通しが良くなった


Reactで短絡評価を使って「<div>Loading...</div>」を出力する場合、

<>
  {
    isLoading && <div>Loading...</div>
  }
</>

制御フローのヘルパー関数のおかげでreturn内の見通しが良くなった


SolidJSで同じことをする場合、ヘルパー関数のShowを用いる (whenがtrueならchildrenを表示、falseならfallbackの内容を表示)

<Show when={isLoading()} fallback={<div>記事一覧</div>}>
  <div>Loading...</div>
</Show>

動作デモ

制御フローのヘルパー関数のおかげでreturn内の見通しが良くなった


Reactで三項演算子を使うと…

<>
  {
    条件1 ? (
      <Component1 />
    ) : 条件2 ? (
      <Component2 />
    ) : 条件3 ? (
      <Component3 />
    ) : (
      <Component4 />
    )
  }
</>

制御フローのヘルパー関数のおかげでreturn内の見通しが良くなった

SolidJSの場合、ヘルパー関数のSwitchとMatchを用いる (Switch内を上から順番に評価して、条件に一致するコンポーネントを一つ出力する)

return (
  <Switch fallback={<Component4 />}>
    <Match when={条件1} children={<Component1 />} />
    <Match when={条件2} children={<Component2 />} />
    <Match when={条件3} children={<Component3 />} />
  </Switch>
);

動作デモ

この他にも、配列をイテレーションするForや、コンポーネントをオブジェクトにまとめて直接表示するDynamic等ヘルパーが用意されている

Todoデスクトップアプリを作ってみた!

技術構成


  • SolidJS
  • Sass(Scss)
  • Service Worker + manifest
    • viteのプラグイン(vite-plugin-pwa)を用いてPWA化
  • IndexDB
    • ToDoデータをローカルに保存
  • Vercel(GitHub経由)でデプロイ

アプリを作ってみてわかったこと


  • Sass(Scss)の導入が非常にラク
  • PWA化もラク
  • コンテキスト作成もラク
  • refを用いたDOM操作がReactよりも素直に扱える

Sass(Scss)の導入が非常にラク


yarn add --dev sass」を実行するだけ

PWA化もラク


Viteのプラグイン(vite-plugin-pwa)を入れて、manifestを宣言するだけ


SASSやPWA化のヒントはSolidJS公式のtemplete集にある

コンテキスト作成もラク


SolidJSもReactのカスタムフックと同じ書き方でにロジックを分離することができる

function createTodo() {
  const [count, setCount] = createSignal(0);
  const handleInput = () => setCount((prev) => prev + 1);
  return { count, handleInput };
}

const App1 = () =>{
  const { count, handleInput } = createTodo();
  return (...);
}
const App2 = () =>{
  const { count, handleInput } = createTodo();
  return (...);
}

スコープはそれぞれのコンポーネント内となる

動作デモ

コンテキスト作成もラク


createRootでラップすることでコンテキストとして扱える

function createTodo() {...}
const createTodoCtx = createRoot(createTodo)

const App1 = () =>{
  const { count, handleInput } = createTodoCtx;
  return (...);
}
const App2 = () =>{
  const { count, handleInput } = createTodoCtx;
  return (...);
}

App1で変化があるとApp2も変化する

動作デモ

refを用いたDOM操作がReactよりも素直に扱える


  • Todoデスクトップアプリには再編集機能がある
  • 登録済みのToDoをクリックすると、Inputに切り替わり自動的にフォーカスが当たる様になっている
    • フォーカスはReact同様refでDOMを操作する
    • しかし、SolidJSにはuseRefのようなものが無い
      • どうするのか?

refを用いたDOM操作がReactよりも素直に扱える


変数を宣言して必要なタイミングでfocusを実行するだけ

let inputRef
createEffect(() => {
  inputRef.focus();
  ...
});

return <input type="text" ref={inputRef}>

動作デモ

refを用いたDOM操作がReactよりも素直に扱える


refを関数として扱うことも可能

// elはHTMLInputElement型
const inputRef = (el:HTMLInputElement) => {
  createEffect(() => {
    ...
    el.focus();
  });
}
return <input type="text" ref={inputRef}>

refの動作

ロジック分離する場合はこちらを使う必要がある

refを用いたDOM操作がReactよりも素直に扱える


  • Reactの場合、inputに限らずCanvasやWeb Audio API等はuseRefが必要になるが、呼び出す度に「.current」と書く必要がある
  • SolidJSの場合、変数を呼び出すだけで済むので書き易く、読み易くなる

まとめ


  • 公式のドキュメントやチュートリアル、Play Groundが充実している
  • コードの見た目はReactそっくりだが、中身は完全に別物
  • Reactから移植する場合、再レンダリングされないことに注意する
    • 例)依存配列無しuseEffectはロジックを全て外に出す

参考文献など


ご清聴ありがとうございました