Skip to content

外部ルーター ​

シェルでTanStack Router、React Router、またはその他のルーターがすでにナビゲーションを担っている場合は、その役割をそのまま任せます。orchestrator.connectRouter()はオーケストレーターをそのルーターに追従させ、interceptLinks()はマイクロアプリ内の通常の<a href>のクリックをそのルーターに送ります。

使うべき場面 ​

  • シェルが独自のルーターを持つReactアプリで、一部のルートがマイクロアプリをホストしている。
  • マイクロアプリが通常のリンク(Vue、Svelte、AngularまたはHTMLテンプレート)をレンダリングしていて、ページを再読み込みせずにナビゲーションさせたい。

シェルが独自のルーターを持たない場合は、代わりに組み込みのルーターを使います。ルーティングを参照してください。

connectRouter ​

ts
import { createOrchestrator, type RouterSource } from 'tuvix.js';

const source: RouterSource = {
  // Call `callback` after every navigation; return an unsubscribe function.
  subscribe(callback) {
    window.addEventListener('popstate', callback);
    return () => window.removeEventListener('popstate', callback);
  },
  // The path activeWhen patterns are matched against.
  getPath: () => window.location.pathname,
};

const orchestrator = createOrchestrator(); // no `router` option
const disconnect = orchestrator.connectRouter(source);
await orchestrator.start();

// later
disconnect();
  • start()はgetPath()の値に対して状態を同期(reconcile)します。その後は、subscribeのコールバックが呼ばれるたびに、新しいgetPath()の値に対する同期がキューに入ります。
  • すでに開始済みのオーケストレーターを接続すると、すぐに同期します。
  • 同期は1つずつ実行され、次に実行されるのはキュー内の最新のパスだけです。そのため、ルーターイベントが連続して発生しても、確定したパスごとに1回の同期で済みます。
  • destroy()は接続されているすべてのルーターを切断します。

TanStack Router ​

ts
import { createOrchestrator, interceptLinks } from 'tuvix.js';
import { router } from './router'; // your createRouter({ routeTree }) instance

const orchestrator = createOrchestrator();

orchestrator.register({
  name: 'blog',
  entry: () => import('./micro-apps/blog'),
  container: '#micro-app',
  activeWhen: '/blog/*',
});

orchestrator.connectRouter({
  subscribe: (callback) => router.subscribe('onResolved', callback),
  getPath: () => router.state.location.pathname,
});

// Plain <a href> inside micro apps → TanStack history.
interceptLinks({ navigate: (path) => router.history.push(path) });

await orchestrator.start();

onResolvedは、ルーターが新しいロケーションを解決した時点で発火します。onLoadとsetTimeoutの組み合わせの代わりにこれを使います。

React Router ​

createBrowserRouterで作成したデータルーターの場合:

ts
import { createOrchestrator, interceptLinks } from 'tuvix.js';
import { router } from './router'; // createBrowserRouter(routes)

const orchestrator = createOrchestrator();

orchestrator.register({
  name: 'account',
  entry: () => import('./micro-apps/account'),
  container: '#micro-app',
  activeWhen: '/account/*',
});

orchestrator.connectRouter({
  subscribe: (callback) => router.subscribe(() => callback()),
  getPath: () => router.state.location.pathname,
});

interceptLinks({ navigate: (path) => router.navigate(path) });

await orchestrator.start();

<BrowserRouter>には購読できるルーターオブジェクトがありません。useLocation()を読み取るコンポーネントから橋渡しします。

tsx
import { useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
import type { Orchestrator, RouterSource } from 'tuvix.js';

export function TuvixBridge({ orchestrator }: { orchestrator: Orchestrator }) {
  const location = useLocation();
  const listeners = useRef(new Set<() => void>());
  const path = useRef<string>(location.pathname);
  path.current = location.pathname;

  useEffect(() => {
    const source: RouterSource = {
      subscribe(callback) {
        listeners.current.add(callback);
        return () => listeners.current.delete(callback);
      },
      getPath: () => path.current,
    };
    return orchestrator.connectRouter(source);
  }, [orchestrator]);

  useEffect(() => {
    for (const callback of listeners.current) callback();
  }, [location.pathname]);

  return null;
}

マイクロアプリ内の通常のリンク ​

ルーター自身の<Link>コンポーネントは、すでにクライアントサイドでナビゲーションします。マイクロアプリが通常の<a href>としてレンダリングするリンクはそうではなく、interceptLinksがなければページを再読み込みします。interceptLinksは修飾キー付きのクリック、別オリジン、target="_blank"、download、rel="external"、data-tuvix-reload、現在のパス上のハッシュのみのリンクをスキップし、それ以外ではnavigateを呼び出します。prefetchを追加すると、ホバーやフォーカスの時点で次のルートを事前に準備できます。TanStack Routerの場合:

ts
import { interceptLinks } from 'tuvix.js';
import { router } from './router';

const stop = interceptLinks({
  navigate: (path) => router.history.push(path),
  prefetch: (path) => void router.preloadRoute({ to: path }),
});

シェルがアンマウントされるときにstop()を呼び出します。

ベースパス ​

ルーターのbasenameが/appなどの場合:

  • activeWhenのパターンは、getPath()が返す文字列そのものに対して照合します。ルーターが/app付きのパスを返す場合は'/app/blog/*'と書くか、getPathでベースを取り除きます。
  • interceptLinks({ base: '/app', ... })は/appの外にあるリンクを無視し、navigateを呼び出す前に/appを取り除きます。ルーターのnavigateがbasename相対のパスを期待する場合(React Routerのrouter.navigate)に使います。navigateがURLのフルパスを受け取る場合(TanStackのrouter.history.push)はbaseを指定しません。

ルートがレンダリングするコンテナ ​

オーケストレーターはマウント時にcontainerを検索します。ルートコンポーネントがコンテナ要素をレンダリングする場合、ルーターのコールバックが発火した時点ではまだDOMに存在しないことがあり、マウントはContainer "#micro-app" not found in DOMで失敗します。回避策は2つあります。

  • マイクロアプリのすべてのルートが共有するレイアウトにコンテナを置き、ナビゲーションの前に存在するようにする。
  • ルートコンポーネント自体からTuvixIsland(@tuvix.js/react)でマイクロアプリをレンダリングし、そのアプリにはオーケストレーターを使わない。

connectRouterが行わないこと ​

  • ナビゲーションは行いません。ナビゲーションはルーターが担います。
  • ルートパラメーターは読み取りません。updateAppPropsでプロパティとして渡すか、アプリ内で読み取ります。
  • リンクのクリックは処理しません。そのためにはinterceptLinksを呼び出します。