はじめに
フロントエンド界隈、新しいフレームワークが出ては消えを繰り返していますよね。正直なところ「また新しいの?」という気持ちになることも多いです。
でもRippleは少し毛色が違う。作者の@trueadmは、Inferno、React、Lexical、そしてSvelte 5にも貢献してきた人物。つまり、既存の主要フレームワークの内部を知り尽くした人が「じゃあ俺が作るとこうなる」と言わんばかりに出してきたフレームワークなんですよ。
GitHubで6,350スター、10ヶ月で48人のコントリビューター。早期開発段階ながら、かなりの注目を集めています。
「React、Solid、Svelteの最良部分を組み合わせた」という触れ込み。これ、言うだけなら誰でも言える。でも実際に中身を見てみると、確かに各フレームワークのいいとこ取りをしようとしている設計思想が見えてきます。
Rippleとは
Rippleは、TypeScriptファーストで設計された新しいUIフレームワークです。
最大の特徴は、track()関数と@演算子による独自のリアクティビティシステム。Svelteの「書きやすさ」とSolidの「ファイングレインリアクティビティ」を両立させようとしています。
.rippleという独自のファイル拡張子を使い、コンポーネントごとにTypeScript、JSX、スコープ付きCSSをまとめて記述できます。Vue SFCやSvelteに近い感覚ですね。
MITライセンスなので商用利用も問題なし。
特徴・メリット
1. track()と@による直感的なリアクティビティ
個人的には、これが一番面白いポイントだと思っています。
component Counter {
let count = track(0)
<button onClick={() => @count++}>
Count: {@count}
</button>
}
track()で変数をリアクティブにして、@でアクセスする。変更があると自動的にUIが更新されます。
ReactのuseStateより書きやすく、Svelteのコンパイラマジックほどブラックボックスでもない。このバランス感覚、30代になって思うのは「結局シンプルが一番」ということなんですよね。
2. 業界トップクラスのパフォーマンス
公式サイトでは「業界をリードするレンダリング速度、バンドルサイズ、メモリ使用量」を謳っています。
これ、実際にどの程度なのかはベンチマーク待ちですが、Inferno(最速クラスの仮想DOMライブラリ)の作者が関わっているので、期待はできる。Solidのようなファイングレインリアクティビティを採用しているので、理論上はReactより効率的なはず。
3. TypeScriptファースト
最初からTypeScriptで設計されていて、.rippleファイルでも完全な型チェックが効きます。
VSCode拡張機能も用意されていて、構文ハイライト、IntelliSense、リアルタイム診断に対応。開発体験はかなり考えられている印象。
Prettierプラグイン、ESLintプラグインもあるので、既存のツールチェーンに組み込みやすい。
4. ネイティブな制御フロー
テンプレート内でif、for、tryがそのまま使えます。
component UserList {
let users = track(#[])
<div>
for (const user of @users) {
<UserCard user={user} />
}
</div>
}
React JSXの{items.map(item => ...)}より読みやすい。Svelteの{#each}より馴染みやすい。普通のJavaScriptの知識がそのまま活きる。
5. リアクティブコレクション
配列やオブジェクトもリアクティブに扱えます。
let items = track(#[1, 2, 3]) // リアクティブ配列
let config = track(#{ // リアクティブオブジェクト
theme: 'dark',
lang: 'ja'
})
#[]と#{}という構文でリアクティブなコレクションを作成。中身を変更すると自動的にUIに反映されます。
6. スコープ付きスタイル
コンポーネント内で<style>を書けば、自動的にスコープされます。
component Button {
<button class="primary">Click me</button>
<style>
.primary {
background: blue;
color: white;
}
</style>
}
グローバルCSSの衝突を気にしなくていい。CSS ModulesやStyled Componentsを別途入れる必要がない。QOL上がりますね。
インストール方法
CLIを使用(推奨)
一番簡単な方法です。
npx create-ripple
cd my-app
npm install && npm run dev
対話形式でプロジェクト名を入力すると、セットアップが完了します。
テンプレートから
npx degit Ripple-TS/ripple/templates/basic my-app
cd my-app
npm install && npm run dev
既存のテンプレートをクローンしてカスタマイズしたい場合はこちら。
既存プロジェクトに追加
Viteプロジェクトに追加する場合:
npm install ripple @ripple-ts/vite-plugin
vite.config.tsに設定を追加:
import { defineConfig } from 'vite'
import ripple from '@ripple-ts/vite-plugin'
export default defineConfig({
plugins: [ripple()]
})
基本的な使い方
コンポーネントの定義
.rippleファイルでコンポーネントを定義します。
// Counter.ripple
component Counter {
let count = track(0)
function increment() {
@count++
}
function decrement() {
@count--
}
<div class="counter">
<button onClick={decrement}>-</button>
<span>{@count}</span>
<button onClick={increment}>+</button>
</div>
<style>
.counter {
display: flex;
gap: 8px;
align-items: center;
}
button {
padding: 8px 16px;
cursor: pointer;
}
</style>
}
Reactと違ってreturn文がない。マークアップを直接書く感じはSvelteに近い。
Propsの受け取り
component Greeting {
props {
name: string
age?: number
}
<div>
<h1>Hello, {name}!</h1>
if (age) {
<p>You are {age} years old.</p>
}
</div>
}
TypeScriptの型定義がそのまま使えるので、型安全にpropsを扱えます。
派生値(Computed)
component PriceCalculator {
let price = track(1000)
let quantity = track(1)
// 派生値:依存値が変わると自動更新
let total = track(() => @price * @quantity)
let taxIncluded = track(() => @total * 1.1)
<div>
<input type="number" value={@price} onChange={e => @price = +e.target.value} />
<input type="number" value={@quantity} onChange={e => @quantity = +e.target.value} />
<p>合計: {Math.floor(@taxIncluded)}円(税込)</p>
</div>
}
track()に関数を渡すと、依存している値が変わったときに自動で再計算されます。VueのcomputedやSolidの派生シグナルに相当する機能。
子コンポーネントの使用
// App.ripple
import Counter from './Counter.ripple'
import Greeting from './Greeting.ripple'
component App {
<main>
<Greeting name="太郎" age={32} />
<Counter />
</main>
}
通常のES Modulesとしてインポートして使えます。
実践的なユースケース
1. Todoリストアプリ
公式サイトでも紹介されている定番の例。
component TodoList {
let todos = track(#[])
let newTodo = track('')
function addTodo() {
if (@newTodo.trim()) {
@todos.push({ id: Date.now(), text: @newTodo, done: false })
@newTodo = ''
}
}
function toggleTodo(id: number) {
const todo = @todos.find(t => t.id === id)
if (todo) todo.done = !todo.done
}
<div class="todo-app">
<input
value={@newTodo}
onChange={e => @newTodo = e.target.value}
onKeyPress={e => e.key === 'Enter' && addTodo()}
placeholder="新しいタスク..."
/>
<button onClick={addTodo}>追加</button>
<ul>
for (const todo of @todos) {
<li class={todo.done ? 'done' : ''} onClick={() => toggleTodo(todo.id)}>
{todo.text}
</li>
}
</ul>
</div>
<style>
.done { text-decoration: line-through; opacity: 0.5; }
</style>
}
リアクティブ配列の変更がそのままUIに反映される。直感的で書きやすい。
2. フォームバリデーション
派生値を使ったリアルタイムバリデーション。
component SignupForm {
let email = track('')
let password = track('')
let emailError = track(() => {
if (!@email) return ''
return @email.includes('@') ? '' : 'メールアドレスの形式が正しくありません'
})
let passwordError = track(() => {
if (!@password) return ''
return @password.length >= 8 ? '' : 'パスワードは8文字以上必要です'
})
let isValid = track(() => !@emailError && !@passwordError && @email && @password)
<form>
<div>
<input type="email" value={@email} onChange={e => @email = e.target.value} />
if (@emailError) {
<span class="error">{@emailError}</span>
}
</div>
<div>
<input type="password" value={@password} onChange={e => @password = e.target.value} />
if (@passwordError) {
<span class="error">{@passwordError}</span>
}
</div>
<button disabled={!@isValid}>登録</button>
</form>
}
3. ダッシュボード
複数の状態を組み合わせたダッシュボード画面。
component Dashboard {
let activeTab = track('overview')
let data = track(#{
users: 1250,
revenue: 45000,
orders: 89
})
<div class="dashboard">
<nav>
<button class={@activeTab === 'overview' ? 'active' : ''} onClick={() => @activeTab = 'overview'}>
概要
</button>
<button class={@activeTab === 'analytics' ? 'active' : ''} onClick={() => @activeTab = 'analytics'}>
分析
</button>
</nav>
if (@activeTab === 'overview') {
<div class="cards">
<Card title="ユーザー数" value={@data.users} />
<Card title="売上" value={@data.revenue} />
<Card title="注文数" value={@data.orders} />
</div>
}
if (@activeTab === 'analytics') {
<Analytics />
}
</div>
}
注意点
早期開発段階
これ、意外と重要なポイントです。Rippleはまだ早期開発段階。プロダクション環境での使用は推奨されていません。
- SSR(サーバーサイドレンダリング)は未対応(coming soon)
- 現時点ではSPA(シングルページアプリケーション)のみ
- APIの破壊的変更がある可能性
個人プロジェクトや実験用として触るのはいいですが、業務で採用するのはもう少し待った方がいいかもしれません。
学習リソース
新しいフレームワークなので、Stack OverflowやQiitaの記事はまだ少ない。公式ドキュメントとDiscordが主な情報源になります。
エコシステム
Reactほどのエコシステムはまだない。UIライブラリやサードパーティツールは限られています。
React/Svelte/Solidとの比較
簡単に整理しておきます。
| 項目 | Ripple | React | Svelte | Solid |
|---|---|---|---|---|
| リアクティビティ | track() + @ | useState/useReducer | コンパイラ | createSignal |
| ファイル形式 | .ripple | .jsx/.tsx | .svelte | .jsx/.tsx |
| バンドルサイズ | 小(予定) | 大きめ | 小 | 小 |
| SSR対応 | 未対応 | 対応 | 対応 | 対応 |
| 成熟度 | 早期開発 | 成熟 | 成熟 | 成長中 |
Reactの「エコシステムの豊富さ」、Svelteの「シンプルな記法」、Solidの「パフォーマンス」。Rippleはこれらのいいとこ取りを目指しているわけですが、まだ道半ばという印象です。
まとめ
Rippleを調べてみて感じたこと:
- track()と@構文: 直感的でシンプルなリアクティビティ
- TypeScriptファースト: 型安全な開発ができる
- ネイティブ制御フロー: if/for/tryがそのまま使える
- スコープ付きスタイル: CSS衝突を気にしなくていい
- 著名な作者: Inferno、React、Svelteに貢献した人物
正直なところ、プロダクションで使うにはまだ早い。SSR未対応、エコシステムも発展途上。
でも「将来のスタンダードになるかもしれない」という可能性は感じます。React、Solid、Svelteの知見を持った作者が、それぞれの課題を解決しようとしている。この設計思想自体に学びがある。
時間があるなら、公式サイトのPlaygroundで触ってみることをおすすめします。track()と@の感覚、一度体験すると「なるほどな」と思えるはず。
数年後に「Ripple、あのとき触っておいてよかった」となる可能性は十分にあると思います。