Webアプリケーションのフロントエンドフレームワークといえば、ReactやVue.js、Svelteなどが有名です。これらのフレームワークは、コンポーネントベースでUIを構築できることや、仮想DOMを利用して効率的にUIを更新できることなどが特徴です。
そして、それらを使っていた開発者であるDominic Gannaway氏が新たに開発したフレームワークがRippleJSです。RippleJSは、ReactやVue.js、Svelteなどの良いところを取り入れつつ、よりシンプルで直感的なAPIを提供しています。
本記事では、RippleJSの特徴と基本的な使い方を紹介します。
RippleJSの特徴
RippleJSはコンポーネントベースのアーキテクチャで、再利用性の高いUIコンポーネントを作成できます。また、HTMLファーストではなく、TypeScriptファーストなフレームワークとなっています。

VSCode機能拡張があり、.ripple
という拡張子に対してシンタックスハイライトが提供されます。
RippleJSの使い方
RippleJSは以下のコマンドでインストールできます。
npx degit trueadm/ripple/templates/basic my-app
cd my-app
npm i
そして、 npm run dev
で開発用サーバーが http://localhost:3000/
で立ち上がります。

基本的なテンプレート
RippleJSのコンポーネントは、以下のような形式で記述します。 export component
でコンポーネントをエクスポートし、HTMLのような構文でUIを定義します。
import { track } from 'ripple';
export component App() {
<div class='container'>
<h1>{'Welcome to Ripple!'}</h1>
<div class='counter'>
let count = track(0);
<button onClick={() => @count--}>{'-'}</button>
<span class='count'>{@count}</span>
<button onClick={() => @count++}>{'+'}</button>
</div>
</div>
<!-- スタイル設定 -->
<style>
</style>
}
変数の定義
変数は let
で定義します。さらに track
関数でラップすることで、リアクティブな変数になります。変数を参照する際には、 @
を付けます。
<div class='counter'>
let count = track(0);
<button onClick={() => @count--}>{'-'}</button>
<span class='count'>{@count}</span>
<button onClick={() => @count++}>{'+'}</button>
</div>
変数の変化をトラッキングする
track
関数の第2引数と第3引数にコールバック関数を渡すことで、変数の取得と設定をトラッキングできます。
let count = track(0,
(current) => {
console.log('Count: ', current);
return current;
},
(next) => {
console.log('Count: ', next);
return next;
}
);

配列の扱い
配列をリアクティブに扱うには、 #[...]
で定義します。配列の長さや要素の追加・削除などがリアクティブに反映されます。
import { effect, track } from 'ripple';
export component App() {
const items = #[1, 2, 3];
<div>
<p>{"Length: "}{items.length}</p> // Reactive length
for (const item of items) {
<div>{item}</div>
}
<button onClick={() => items.push(items.length + 1)}>{"Add"}</button>
</div>
}

オブジェクトの扱い
オブジェクトをリアクティブに扱うには、 #{...}
で定義します。オブジェクトのプロパティの変更がリアクティブに反映されます。
export component App() {
const obj = #{a: 0, b: undefined}
obj.a = 0;
<pre>{'obj.a is: '}{obj.a}</pre>
<pre>{'obj.b is: '}{obj.b}</pre>
<button onClick={() => { obj.a++; obj.b = obj.b ?? 5; obj.b++; }}>{'Increment'}</button>
}

特殊な型の扱い
Set
や Map
、 Date
などの特殊な型もリアクティブに扱うことができます。以下に例を示します。
import { TrackedSet, track } from 'ripple';
export component App() {
const set = new TrackedSet([1, 2, 3]);
// direct usage
<p>{"Direct usage: set contains 2: "}{set.has(2)}</p>
// reactive assignment
let has = track(() => set.has(2));
<p>{"Assigned usage: set contains 2: "}{@has}</p>
<button onClick={() => set.delete(2)}>{"Delete 2"}</button>
<button onClick={() => set.add(2)}>{"Add 2"}</button>
}
Map
型の場合は TrackedMap
を使います。
import { TrackedMap, track } from 'ripple';
export component App() {
const map = new TrackedMap([[1,1], [2,2], [3,3], [4,4]]);
// direct usage
<p>{"Direct usage: map has an item with key 2: "}{map.has(2)}</p>
// reactive assignment
let has = track(() => map.has(2));
<p>{"Assigned usage: map has an item with key 2: "}{@has}</p>
<button onClick={() => map.delete(2)}>{"Delete item with key 2"}</button>
<button onClick={() => map.set(2, 2)}>{"Add key 2 with value 2"}</button>
}
日付型の場合は TrackedDate
を使います。
import { TrackedDate, track } from 'ripple';
export component App() {
const date = new TrackedDate(2025, 0, 1, 12, 0, 0);
// direct usage
<p>{"Direct usage: Current year is "}{date.getFullYear()}</p>
<p>{"ISO String: "}{date.toISOString()}</p>
// reactive assignment
let year = track(() => date.getFullYear());
let month = track(() => date.getMonth());
<p>{"Assigned usage: Year "}{@year}{", Month "}{@month}</p>
<button onClick={() => date.setFullYear(2027)}>{"Change to 2026"}</button>
<button onClick={() => date.setMonth(11)}>{"Change to December"}</button>
}
条件分岐
変数の内容によって表示内容を切り替えるには、 switch
文を使います。以下は status
変数の内容によって表示を切り替える例です。
import { track } from 'ripple';
export component App() {
let status = track('loading');
<button onClick={() => @status = 'success'}>{'Success'}</button>
<button onClick={() => @status = 'error'}>{'Error'}</button>
switch (@status) {
case 'loading':
<p>{'Loading...'}</p>
break;
case 'success':
<p>{'Success!'}</p>
break;
case 'error':
<p>{'Error!'}</p>
break;
default:
<p>{'Unknown status'}</p>
}
}

生のHTMLを挿入する
基本的なHTMLはエスケープされますが、生のHTMLを挿入したい場合は html
関数を使います。
export component App() {
let source = `
<h1>My Blog Post</h1>
<p>Hi! I like JS and Ripple.</p>
`
<article>
{html source}
</article>
}

イベントの扱い
RippleJSでは、 effect
関数と on
関数を使ってイベントを扱います。以下は、ウィンドウのリサイズイベントを監視する例です。コンポーネントがアンマウントされた際の処理を返却します。
import { effect, on } from 'ripple';
export component App() {
effect(() => {
// on component mount
const removeListener = on(window, 'resize', () => {
console.log('Window resized!');
});
// return the removeListener when the component unmounts
return removeListener;
});
}

コンテキストの利用
RippleJSにはコンテキストという概念があります。他のフレームワークのように、コンポーネントツリーを渡して値やリアクティブオブジェクトを共有できます。
import { track, Context } from "ripple"
// コンテキストの作成
const context = new Context({ count: undefined });
const context2 = new Context();
export component App() {
// コンテキストを取得
const obj = context.get();
// リアクティブなプロパティを追加
obj.count = track(0);
// 別なのコンテキストにリアクティブなプロパティを追加
const count2 = track(0);
// コンテキストに値をセットする
context2.set(count2);
<button onClick={() => { obj.@count++; @count2++ }}>
{'Click Me'}
</button>
// コンテキストの値を表示
<pre>{'Context: '}{context.get().@count}</pre>
<pre>{'Context2: '}{@count2}</pre>
}

注意点
RippleJSはまだ新しいフレームワークであり、不足している機能もあります。特にSSRはまだ未実装で、近い将来実現されるとのことです。
まとめ
RippleJSはReactやVue.js、Svelteとまた違った使い勝手のUIフレームワークとなっています。まだこれからの機能も多いですが、シンプルで直感的なAPIは魅力的です。興味がある方はぜひ試してみてください。