Skip to content

Bootstrap и mount

Bootstrap — единственный публичный вход в UI remote. Это обычный TypeScript module, который экспортирует функцию mount(options).

mount получает всё необходимое для одного запуска remote и возвращает функцию очистки.

Минимальный контракт без Query Layer

Сначала полезно понять чистый UI lifecycle.

ts
export interface BillingBootstrapOptions {
    // DOM-элемент, внутрь которого remote должен смонтировать React.
    container: HTMLElement
}

export interface BillingRemoteModule {
    // Host вызывает mount и получает disposer.
    mount(options: BillingBootstrapOptions): () => void
}

Реализация remote

ts
import { createRoot } from "react-dom/client"

import { BillingApp } from "./BillingApp"
import type { BillingBootstrapOptions } from "@portal/contracts"

export const mount = (options: BillingBootstrapOptions): (() => void) => {
    // Remote не ищет элемент по глобальному id.
    // Конкретный container выбирает host.
    const root = createRoot(options.container)

    root.render(<BillingApp />)

    // Host вызовет эту функцию при смене route или shutdown.
    return () => {
        root.unmount()
    }
}

Вызов из host

ts
export const mountBilling = async (container: HTMLElement): Promise<() => void> => {
    // Federation plugin разрешает этот динамический import.
    const remote = await import("billing/bootstrap")

    // Host явно передаёт container аргументом.
    return remote.mount({ container })
}

Этот этап проверяет только три вещи:

  1. host действительно загружает remote bundle;
  2. React remote монтируется в правильный container;
  3. disposer полностью выполняет unmount.

Это не обязательная отдельная production-версия. Это способ изолировать ошибки federation loader/React lifecycle от ошибок server-state интеграции.

Как это связано с router

Router решает, когда показать remote. Composition function делает остальную работу:

ts
// Упрощённый route handler host.
let unmountCurrentRoute: (() => void) | null = null

export const openBillingRoute = async (): Promise<void> => {
    // Сначала удаляем UI предыдущего route.
    unmountCurrentRoute?.()

    const container = document.getElementById("route-root")
    if (!container) {
        throw new Error("route-root not found")
    }

    // Router вызвал composition function, а она вызвала remote mount.
    unmountCurrentRoute = await mountBilling(container)
}

Router не обязан хранить runtime или scope. Эти зависимости может замкнуть функция mountBilling из composition layer.

Добавление Query Layer

После понятного mount-контракта расширяем options:

ts
import type { IScopedQueryRuntime } from "@dubium/query-layer/mf"

export interface BillingBootstrapOptions {
    container: HTMLElement

    // Узкий transport contract только для billing feature.
    api: BillingApi

    // Ограниченные store factories одного mount.
    scope: IScopedQueryRuntime
}

Host создаёт оба объекта и передаёт их явно:

ts
const scope = runtime.createParticipantScope(billingRegistration)

const unmountUi = remote.mount({
    container,
    api: billingApi,
    scope,
})

return () => {
    // Сначала React перестаёт использовать stores.
    unmountUi()

    // Затем host освобождает все ресурсы participant.
    scope.dispose()
}

Как scope попадает в глубокие компоненты

Не передавайте scope через каждый React prop. Bootstrap создаёт domain stores или composition object один раз, а компоненты получают уже stores.

ts
export const mount = (options: BillingBootstrapOptions): (() => void) => {
    // Scope и API используются только на composition boundary remote.
    const billingStore = new BillingStore(options.scope, options.api)
    const root = createRoot(options.container)

    // UI получает предметный store, а не инфраструктурный scope.
    root.render(<BillingApp store={billingStore} />)

    return () => {
        root.unmount()
        billingStore.dispose()
    }
}

React Context допустим для набора domain stores, но root runtime всё равно не должен становиться глобальной переменной remote.

Контракт disposer

Функция, возвращённая mount, должна:

  • вызвать root.unmount();
  • освободить domain stores remote;
  • удалить собственные DOM/window listeners;
  • остановить timers remote;
  • быть безопасной при повторном вызове.

Participant scope освобождает host после unmount UI. Полный порядок: Lifecycle MF.