メモを追加して一覧するアプリを作成します。クイックスタートの sobria create で作成した雛形から開始して、pnpm dev で開発サーバーを起動しておきます。
ページでメモの一覧を表示する
メモは、サーバーのメモリ上の配列に保存します。サーバーを再起動すると、配列は最初の状態に戻ります。
// src/lib/db.ts
export const memos: string[] = ["牛乳を買う", "sobria のチュートリアルを読む"];src/routes/page.tsx を次のように置換して、配列の内容を一覧にします。雛形の src/elements/greeter.tsx と src/lib/greet.ts は使用しないので、置換した後に削除します。
// src/routes/page.tsx
import { memos } from "../lib/db.ts";
export default function Page() {
return (
<>
<h1>メモ</h1>
<ul>
{memos.map((memo) => (
<li>{memo}</li>
))}
</ul>
</>
);
}ページはサーバーでだけ実行するので、サーバーのモジュールの値を直接参照できます。ブラウザで http://localhost:5173/ を開くと、2 件のメモが表示されます。sobria は、このページではブラウザに JS を送信しません。
ページのファイルの配置と URL の対応はルーティングで、ページでデータを取得する方法はページとデータで説明します。
要素で入力欄を作成する
メモを入力する欄を、要素として作成します。要素は、ブラウザでも動作するコンポーネントです。ページと異なり、要素の JS はブラウザに送信されます。この章の要素は、入力中の文字数を入力欄の横に表示します。
// src/elements/memo-form.tsx
import { signal } from "sobria";
export default function MemoForm() {
const [text, setText] = signal("");
return (
<div>
<input value={text()} onInput={(e) => setText(e.currentTarget.value)} />
<span>{text().length} 文字</span>
</div>
);
}signal は、読み取りの関数と書き込みの関数の組を返します。text() を参照している箇所は、setText で値を書き込むたびに更新されます。要素の関数は 1 回だけ実行し、それ以降は text() を参照している入力欄と文字数の表示だけを更新します。
ページに要素を配置します。
// src/routes/page.tsx
import MemoForm from "../elements/memo-form.tsx";
import { memos } from "../lib/db.ts";
export default function Page() {
return (
<>
<h1>メモ</h1>
<MemoForm />
<ul>
{memos.map((memo) => (
<li>{memo}</li>
))}
</ul>
</>
);
}ファイル名が memo-form.tsx なので、要素のタグ名は sobria-memo-form になります。要素もサーバーで描画するので、HTML には 0 文字 の表示を含む Shadow DOM が出力されます。JS が届くと要素が起動し、入力に合わせて文字数の表示が変化するようになります。
要素の作り方は要素で、Signal の使い方はリアクティビティで説明します。
サーバー関数でメモを追加する
メモを配列に追加する処理は、サーバーで実行します。サーバー関数とは、要素から呼び出すとサーバーで実行される関数のことです。defineServerFn で定義します。
// src/lib/memos.ts
import { defineServerFn } from "sobria/server";
import { memos } from "./db.ts";
export const addMemo = defineServerFn({
input: (value: { text: string }) => (typeof value?.text === "string" ? value.text.trim() : ""),
})((_c, { input }) => {
if (input !== "") memos.push(input);
});defineServerFn には、1 段目にオプションを、2 段目にサーバーで実行する関数(handler)を渡します。オプションの input に指定した関数は、ブラウザから受け取った値を検証して変換します。この関数が返した値が、handler の input になります。例えば、{ text: " 牛乳 " } を受け取ると、text の前後の空白を除いた "牛乳" を handler に渡します。ブラウザから届く値は型のとおりとは限らないので、text が文字列でなければ空の文字列にします。
要素にボタンを追加して、押したときにサーバー関数を呼び出します。
// src/elements/memo-form.tsx
import { signal } from "sobria";
import { addMemo } from "../lib/memos.ts";
export default function MemoForm() {
const [text, setText] = signal("");
const add = async () => {
await addMemo({ text: text() });
setText("");
};
return (
<div>
<input value={text()} onInput={(e) => setText(e.currentTarget.value)} />
<span>{text().length} 文字</span>
<button onClick={add}>追加</button>
</div>
);
}要素の中で addMemo を呼び出すと、sobria はブラウザからサーバーにリクエストを送信し、サーバーで handler を実行します。ブラウザに送信する JS には、handler と src/lib/db.ts は含まれません。
addMemo は、HTTP の POST で呼び出すサーバー関数です。POST で呼び出すサーバー関数のレスポンスには、呼び出したページをサーバーで描き直した結果が含まれます。sobria はその結果をページに反映してから、await の後の処理に進みます。そのため、ボタンを押すと、追加したメモが一覧に表示されます。
開発サーバーでは、ファイルを保存すると、配列が最初の 2 件に戻ることがあります。
サーバー関数の定義と呼び出しはサーバー関数で説明します。
フォームにして JS が無くても送信できるようにする
前の章のボタンは、要素の JS が動作するまで機能しません。サーバー関数を <form> の action に指定すると、JS が無い環境でもメモを追加できるようになります。
// src/elements/memo-form.tsx
import { signal } from "sobria";
import { addMemo } from "../lib/memos.ts";
export default function MemoForm() {
const [text, setText] = signal("");
return (
<form action={addMemo} onReset={() => setText("")}>
<input name="text" value={text()} onInput={(e) => setText(e.currentTarget.value)} required />
<span>{text().length} 文字</span>
<button>追加</button>
</form>
);
}sobria は、action を出力せずに、送信先のサーバー関数を示す hidden input を出力し、method を post にします。ブラウザは、現在のページの URL にフォームを送信します。フォームの値は、入力欄の name をキーにして { text: "..." } の形でサーバー関数の input に渡ります。前の章で input の形をオブジェクトにしたので、src/lib/memos.ts は変更しません。
JS が無いときは、ブラウザが通常のフォームの送信を実行します。サーバーは handler を実行した後に、送信したページにリダイレクトします。ブラウザは / を読み込み直すので、追加したメモを含む一覧が表示されます。
一方、JS があるときは、sobria がフォームの値をサーバーに送信し、描き直したページの差分を反映してから、form.reset() で入力欄を初期値に戻します。ページの読み込み直しは発生しません。文字数の Signal はリセットで変化しないので、onReset で空の文字列に戻します。
フォームの扱いはフォームで説明します。
ビルドして起動する
pnpm build
pnpm starthttp://localhost:3000/ を開くと、開発サーバーと同じページが表示されます。
Node 以外の環境にデプロイする方法はデプロイで説明します。