Skip to content

ライフサイクル・エラー境界・サスペンス

7.1 ライフサイクルイベント一覧

イベントタイミング
app.startアプリ起動直後(初期 slot 値が確定し、ランタイムがマウントされた時点)
app.stopアプリ終了直前(ブラウザクローズ・タブ閉じる前)
app.error未捕捉エラー発生時
app.http-401HTTP 401 を受信したとき(app.http.on-401 経由で来る)
app.http-403同 403
app.http-5xx同 5xx
app.visibleタブが表示状態になったとき
app.hiddenタブが非表示状態になったとき
app.onlineネットワーク復旧
app.offlineネットワーク切断
route.enter(pattern)ルート進入直後
route.leave(pattern)ルート離脱直前
route.error(pattern)当該ルートの tile 描画中にエラー
tile.mount(name)特定 tile の初回マウント
tile.unmount(name)特定 tile のアンマウント
timer(duration)指定間隔で繰り返し

7.1.1 app.start

アプリ起動時に 1 回だけ発火。app.init = [...] で宣言した effect 列が emit されたに届く。

strand
reducer boot
    on=app.start
    do= emit loadSession()
        emit loadTodos()
        emit identify(currentUser())

app.start reducer の中で emit した effect は synchronous に dispatcher へ渡される(reducer の戻り値として)。dispatcher は capability を check し、policy に従って実行する。

7.1.2 app.stop

ブラウザが beforeunload を発火したタイミング。短時間で完了する処理のみ実行可能(ブラウザ仕様)。

strand
reducer cleanup
    on=app.stop
    do= emit persist(todos)         ; 同期 storage.write のみ実用的

7.1.3 app.visible / app.hidden

visibilitychange イベントに対応。タブ切り替えで状態をポーズしたい場合:

strand
reducer pause on=app.hidden  do= timerPaused := true
reducer resume on=app.visible do= timerPaused := false
                                 emit syncFromServer()

7.1.4 app.online / app.offline

strand
reducer onlineSync   on=app.online   do= emit retryQueued()
reducer showOffline  on=app.offline  do= emit toast({kind: "warn", text: "Offline"})

7.1.5 timer

strand
reducer poll
    on=timer(5s)
    do= emit fetchUpdates()

timer(d) は app の mount 時から d 間隔で繰り返し発火する。ランタイム実装は setInterval ベースで、appdispose 時に自動 clear される。stop-timer(name) での明示停止は v0.2 で追加予定。

duration リテラル: 1ms, 500ms, 1s, 30s, 5m のように整数 + 単位 (ms / s / m) で書ける。

strand
reducer tick on=timer(1s)   do= elapsed := elapsed + 1
reducer poll on=timer(30s)  do= emit fetchUpdates()
reducer fast on=timer(100ms) do= emit syncCursor()

7.1.6 tile.mount / tile.unmount

特定の tile が DOM に現れた / 消えたタイミング。

strand
reducer trackPageView
    on=tile.mount(SettingsPage)
    do= emit track({event: "settings_view", props: {}})

複数の tile を一度に対象にしたい場合は同名の reducer を複数定義する(定義順で実行)。


7.2 エラー処理

Strand では try/catch を許可しない。エラーは次の経路で扱う:

7.2.1 期待されるエラー

Result(T, E) 型で表現する。effect の戻り値が Result.Err の場合は effect-name.err($e, $k) reducer に届く。

7.2.2 想定外のエラー(panic)

  • reducer 内での Option.get で None を取った
  • List.get(i) で範囲外
  • Result.get で Err
  • panic(msg) の明示呼び出し

これらは panic と呼ばれる例外。panic は episode log に記録され、現在の reducer は中断される。slot の変更は トランザクション的にロールバックされる。

7.2.3 app.error reducer

strand
slot lastError : Option(PanicInfo) = None

reducer onPanic
    on=app.error
    do= lastError := Some($event)
        emit log({level: "error", message: $event.message, data: {}})
        emit toast({kind: "error", text: "Something went wrong"})

PanicInfo の型:

strand
type PanicInfo = {
    message: Text,
    location: Text,         ; "reducer:foo:line:42"
    episode-id: Text,
    cause: Option(Text)
}

7.3 エラー境界(タイル単位)

特定の tile 配下の描画エラーを捕捉して fallback を出す:

strand
tile UserPage
    error-boundary = ErrorFallback
    = page(
        UserHeader,
        UserStats,
        UserActivity)

tile ErrorFallback
    in=PanicInfo
    = column(
        heading("Something went wrong"),
        text($1.message) {color: "danger"},
        button(text="Retry", onClick=retryUserPage))

error-boundary = X を tile 定義に書くと、その tile 配下の描画中 panic は X tile を in=PanicInfo で呼び出して fallback 表示する。


7.4 サスペンス(loading 表示)

非同期 effect の結果待ちで loading 表示したい場合。Strand は 明示的に LoadResult(T) 型を使うことを推奨する:

strand
type LoadResult(T) = Idle | Loading | Loaded(T) | Failed(HttpError)

slot user : LoadResult(User) = Idle

tile UserView = match user with
                  | Idle      -> button(text="Load", onClick=fetchUser)
                  | Loading   -> spinner() {size: "lg"}
                  | Loaded(u) -> UserCard(u)
                  | Failed(e) -> ErrorView(e)

専用の <Suspense> 機構は持たない(Reactで起こった「どこから何が suspend するか追跡困難」を避けるため)。

7.4.1 match 式

ebnf
match-expr ::= 'match' expr 'with' match-arm+
match-arm  ::= '|' pattern '->' expr
pattern    ::= identifier                              ; variant 名
             | identifier '(' bind (',' bind)* ')'     ; variant + 束縛
             | '_'                                     ; ワイルドカード
bind       ::= identifier

network コードはほぼ常に match で書く。これは Strand における loading/error の正規パターン。


7.5 404 と error ページ

7.5.1 404

/404 への到達は通常のルートと同じ。ルートマッチに失敗するとランタイムが nav.replace/404 に飛ばす。

strand
tile NotFound = page(
                  heading("404"),
                  text("Page not found"),
                  link(to="/") {text: "Home"})

7.5.2 ルート単位のエラー fallback

strand
reducer onRouteErr
    on=route.error("/todos/:id")
    do= toastError := Some("Failed to load todo")
        emit navigate-replace({path: "/todos", params: {}, query: {}})

7.6 確認ダイアログ

Strand は window.confirm 相当を effect として提供する:

strand
effect confirm cap=notification.show
               in={title: Text, message: Text, onYes: ReducerRef, onNo: ReducerRef}
               out=Unit

reducer askDelete
    on=ui.click(DeleteBtn)
    do= emit confirm({
            title: "削除しますか?",
            message: "この操作は取り消せません",
            onYes: doDelete,
            onNo:  noop
        })

reducer doDelete on=ui.click(_) do= ...     ; ※ 実装上は別名 reducer を作る方が綺麗
reducer noop     on=ui.click(_) do= ()

ランタイム実装ではこれは モーダルダイアログ tile として描画される(ネイティブ confirm ではない)。これにより UI スタイルが揃い、テストも容易になる。


7.7 トースト

strand
effect toast cap=notification.show
             in={kind: Text, text: Text, duration: Option(Duration)}
             out=Unit

reducer notifySave
    on=persist.ok(_, _)
    do= emit toast({kind: "success", text: "Saved", duration: Some(Duration.s(3))})

kindinfo / success / warning / error のいずれか。duration 未指定なら kind 別のデフォルト(info 3s, success 3s, warning 5s, error 0=手動閉じ)。

ランタイムは画面右下にトーストスタックを管理する組み込み tile を持つ。


7.8 アクセシビリティの最小規約

規約適用
button には必ず text または aria-labelコンパイル時警告
image には必ず altコンパイル時警告
link には必ず内側テキストか aria-labelコンパイル時警告
form 内の input には対応する labelコンパイル時警告
キーボード操作 (Tab/Enter/Esc) はランタイムが自動ランタイム保証
フォーカス管理: modal は trap focusランタイム保証
aria-live 領域: toasterror で自動ランタイム保証

これらは「警告」レベルで、コンパイルは通る。--strict-a11y フラグで警告をエラーに昇格できる。


7.9 ホットリロード時の状態

開発時のホットリロードで slot 値を保持するか破棄するか:

slot 修飾子reload 時
なし維持
transient破棄(初期値に戻る)
volatile永続化対象から外す(log にも書かれない、reload で破棄)
strand
slot draft : Text             = ""        ; reload で維持
slot toast : Option(Toast)    transient = None  ; reload で破棄
slot password : Text          volatile  = ""    ; episode log にも書かれない

7.10 設計上の判断記録

判断理由
try/catch を許さないエラー伝播が暗黙になる、Result で明示
panic 時に slot をロールバック中途半端な状態を残さない
Suspense 専用機構を持たないLoadResult 型で明示する方が AI に追跡しやすい
confirm を effect として提供UI 一貫性とテスト容易性
a11y を警告レベルで強制機械的にチェックできる項目は構造で守る
エラー境界を tile 属性にtile 階層と error 階層を一致させる

7.11 次