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>
}

特殊な型の扱い

SetMapDate などの特殊な型もリアクティブに扱うことができます。以下に例を示します。

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は魅力的です。興味がある方はぜひ試してみてください。

RippleJS