feature image

2023年12月24日 | ブログ記事

2024年はSolid.jsを使いませんか?【部内PaaS基盤 NeoShowcase フロント開発ログ】

この記事はtraPアドベントカレンダー2023 24日目(12/24)の記事です。

こんにちは@d_etteiu8383です。@eyemono.moeでもあります。めりくり~

2週間ほど前、部内PaaS基盤であるNeoShowcasev1.0.0がリリースされました(現在はv1.1.2がリリースされています)。
このリリースではフロントエンドの実装が一新され、新UIが導入されました。

新UIのトップ画面

このフロントエンドの実装の大半を私と@tokiが担当しました。本記事ではそのフロントエンドの開発で使用した技術についてご紹介します。

なお、NeoShowcaseについての詳細な紹介は後日別の記事として投稿予定です。しばしお待ちを。

技育展2023でNeoShowcaseを紹介した際のスライド↓

使用技術

フロントのpackage.jsonNeoShowcase/dashboard/package.json at main · traPtitech/NeoShowcase

Solid

solidjs.com

GitHub - solidjs/solid: A declarative, efficient, and flexible JavaScript library for building user interfaces.
A declarative, efficient, and flexible JavaScript library for building user interfaces. - GitHub - solidjs/solid: A declarative, efficient, and flexible JavaScript library for building user interfa…

フロントのフレームワークとしてSolidを使用しています。
いわゆる宣言型UIフレームワークの一つで、仮想DOMを使用していないのが特徴です。

個人的にはReactやVueよりも好きなのですがいまいち流行ってないかも...? という印象です。Solidの良さを伝えるためにもNeoShowcaseでアピールしていきたい。
以前までは私も、まだプロダクトに使える程エコシステムが整っていないと感じていましたが、最近はかなり整ってきていると感じています。2024年はSolidを使いませんか?

NeoShowcaseはPaaSということもあってフォームの入力が多いのですが、後述するModular Formsというライブラリを使うことでフォームの実装がかなり楽になりました。
Solidはやはり状態管理をシンプルに書けるのが使っていて気持ちいいです。

とはいえ不満点もまだあります。Solidには<Show>という制御フローコンポーネントが用意されており、

<Show when={user} fallback={<>Loading...</>}>
  Hello, {user.name}!
</Show>

のように記述することで、whentruthyなら子要素を、falsyならfallbackを表示するということができます。

しかし、solidjs.com/guides/typescript#制御フローの絞り込みでも触れられているのですが、<Show>コンポーネント内でtype narrowingが行われません。これのためにNeoShowcaseではlinterの設定でnoNonNullAssertionを許容しています。

Solid Router

GitHub - solidjs/solid-router: A universal router for Solid inspired by Ember and React Router
A universal router for Solid inspired by Ember and React Router - GitHub - solidjs/solid-router: A universal router for Solid inspired by Ember and React Router

Solid RouterはSolid.jsにおけるroutingライブラリです。React Router等のroutingライブラリと同様の機能を提供していますが、Solid Router v0.10.0でpreload機能が追加されたためご紹介します。

詳細はREADMEのData APIsセクションで紹介されていますが、

  1. ページ表示に必要なデータの読込にcache関数を使用する
  2. Routeコンポーネントのload属性に、1. の関数を実行する関数を渡す

といった書き方をすることで、そのrouteへのリンクのホバー時にデータを事前取得することができます。

/src/libs/api.tsexport const getRepository = cache((id) => client.getRepository({ repositoryId: id }), 'repository')
/src/routes.tsxconst loadRepositoryData: RouteLoadFunc = ({ params }) => {
  void getRepository(params.id)
}

<Route path="/repos/:id" component={lazy(() => import('/@/pages/repos/[id]'))} load={loadRepositoryData} />

以下の動画はアプリケーション一覧ページでのpreloadの挙動の例です。各リンクにカーソルをホバーさせると、対応したデータがfetchされます。リンクをクリックしてそのページに遷移した場合、重複したデータのロードは行われず、事前に読み込まれたデータが使用されます。

0:00
/
リンクホバー時にGetApplicationsとGetRepositoryがpreloadされている

(つまりReact RouterにおけるRouteコンポーネントのloader属性useLoaderData, Remixにおけるそれなどと同じことがSolid Routerでもできるようになった)

(NeoShowcase v1.0.0リリース直前にこのSolid Router v0.10.0がリリースされて大幅に書き方が変わったのですがその対応は全部@tokiさんがやってくださいました)

Modular Forms

Modular Forms: The modular and type-safe form library

Modular FormsはSolidに対応したフォームライブラリです。Zod等を用いたバリデーションにも対応しています。
ネストしたオブジェクトや配列を持つフォームも簡単に実装でき、複雑なformの多いNeoShowcaseでは非常に助かりました。

以下はGuide: Add fields to form (SolidJS) | Modular Formsにある公式のサンプルなのですが、Modular Formsではこのように<Field>コンポーネントを介してformの状態を管理しています。

import { createForm, SubmitHandler } from '@modular-forms/solid';

type LoginForm = {
  email: string;
  password: string;
};

export default function App() {
  const [loginForm, { Form, Field }] = createForm<LoginForm>();

  const handleSubmit: SubmitHandler<LoginForm> = (values, event) => {
    // Runs on client
  };

  return (
    <Form onSubmit={handleSubmit}>
      <Field name="email">
        {(field, props) => <input {...props} type="email" />}
      </Field>
      <Field name="password">
        {(field, props) => <input {...props} type="password" />}
      </Field>
      <button type="submit">Login</button>
    </Form>
  );
}

ここで重要なのが、<Field>コンポーネントがDOMに含まれるときのみ、そのフォームフィールドの状態がアクティブになる点です。先述した<Show>等を使って<Field>をDOMに含めないようにすることで、そのフィールドをSubmit時に含めないと行ったことができます。
(https://modularforms.dev/solid/guides/add-fields-to-form#active-state)

NeoShowcaseでのアプリビルド設定では、デプロイタイプ(runtimeか静的か)とビルドタイプ(buildpack/command/dockerfile)に応じて送信すべきフィールドが異なるのですが、この仕組みを使うことで比較的シンプルに実装することができました。

新UIのビルド設定画面

Kobalte

Introduction – Kobalte

KobalteはSolidのヘッドレスUIコンポーネントライブラリです。今年生まれたばかりのライブラリですが、すでに十分に使えるレベルになっています。NeoShowcaseではPopoverやDialogの表示にKobalteを使用しています。アクセシビリティにも配慮されていて良い感じです。

元々はModalやらSelectやらを自前で実装していたのですが、使い勝手が悪かったためKobalteに置き換えました。
先述したModular FormsのドキュメントにKobalteを使用したフォームの作成例が挙げられており、これを参考に実装しています。

macaron

macaron — Colocated CSS-in-JS with zero-runtime
Typesafe CSS-in-JS with zero runtime, colocation, maximum safety and productivity. Macaron is a new compile time CSS-in-JS library with type safety.

macaronはゼロランタイムのCSS-in-JSライブラリです。

ほとんどvanilla-extractじゃねーか!と思われるかもしれませんが(というか内部でvanilla-extractを使っている)、大きな違いとして、macaronではスタイルとコンポーネントを同じファイルに書くことができます。(vanilla-extractはスタイルを別ファイルに書いてexportし、コンポーネント側でimportする必要がある)

NeoShowcaseでも元々はvanilla-extractを使っていましたが、ファイル分けを強制されるのが辛くてmacaronに乗り換えています。なによりロゴがかわいい。

vanilla-extractではスタイルを適用するためのclass名が生成されますが、macaronでは下記のようにstyled関数を使って直接コンポーネントを生成することができます。

import { styled } from '@macaron-css/solid'

const MyButton = styled('button', {
  base: {
    width: 'auto',
    display: 'flex',
    // ...
    selectors: {
      '&:disabled': {
        cursor: 'not-allowed',
        border: 'none !important',
        color: `${colorVars.semantic.text.black} !important`,
        background: `${colorVars.semantic.text.disabled} !important`,
      },
      // ...
    },
  },
  variants: {
    full: {
      true: {
        width: '100%',
      },
    },
    type: {
      primary: {
        background: colorVars.semantic.primary.main,
        color: colorVars.semantic.text.white,
        selectors: {
          // ...
        },
      },
      // ...
    },
  },
})


// you can use it like this
() => <MyButton full type="primary">Click me!</MyButton>

variantsの使い勝手が非常に良くて非常に良い感じです。

これまで

今年の4月下旬にSysAd班に入り、「Solid好きならNeoShowcaseのフロントやらん?」って感じでNeoShowcaseのフロントの開発に参加することになりました。
(私が開発に参加する前からNeoShowcaseのフロントはSolidで一部実装されていた && もともと私がSolid推しだった)

ということでNeoShowcaseのプロジェクト自体には今年の5月ごろから参加していたのですが、10月頃まで諸々の事情によりあまり活動できていませんでした。ごめんなさい。

5月時点でバックエンドの開発がほぼ完了しており、仮UIとしてまずは機能部分に集中してフロントを完成させることを目標にしていました。
6月に開催を予定していたサークル内ハッカソンでの利用を見越しての仮UI実装でしたが、実際にいくつかのチームが作品の公開にNeoShowcaseを使ってくれました。うれし。

7月頃には@pikachuさんもフロントの開発を進めてくださいました。対照的に私の開発参加が減っていきました。ゆるしてください。
9月頃にはなんか@tokiさんが全部やってました。

10月に入ってから本格的に開発を再開しました。この時点ですでに@tarariraさんによる新UIデザインが上がっており、これを元に実装を進めていきました。めちゃくちゃデザインがかっこよかったので実装するのもめちゃくちゃ楽しかったです。これまで開発に参加できなかった分、積極的にやっていかねば...の精神でガッとやりました。(結局二か月かかったし後半また@tokiさんが全部やってた)

これから

UIの改修はまだまだ続きます。現在レスポンシブ対応が不十分であり、一部ページでボタン等が隠れてしまうのですが、部員から「モバイル端末でもNeoShowcase上で操作したい」との声が上がっているため、レスポンシブ対応を進めていきたいです。アプリケーション・ビルドの情報表示についても、よりわかりやすいものに改修していきたいです。

おわりに

NeoShowcaseのフロントエンドの開発についてご紹介しました。
Solid.jsによる開発は少なくとも私にとっては快適でした。皆さんも2024年はSolid.jsを使ってみてはいかがでしょうか?


traPアドベントカレンダー2023も本日で最終日となりました。他の記事もぜひご覧ください!

d_etteiu8383 icon
この記事を書いた人
d_etteiu8383

グラフィック班とゲーム班とSysAd班所属 いろいろ活動しています

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2023年12月11日
DIGI-CON HACKATHON 2023『Mikage』
toshi00 icon toshi00
2021年8月12日
CPCTFを支えたWebshell
mazrean icon mazrean
2021年5月19日
CPCTF2021を実現させたスコアサーバー
xxpoxx icon xxpoxx
2023年12月13日
HgameOPについて語る
noc7t icon noc7t
2022年9月26日
競プロしかシラン人間が web アプリ QK Judge を作った話
tqk icon tqk
2023年12月25日
オレオレRustプロジェクトテンプレート
H1rono_K icon H1rono_K
記事一覧 タグ一覧 Google アナリティクスについて