外部ルーター
シェルでTanStack Router、React Router、またはその他のルーターがすでにナビゲーションを担っている場合は、その役割をそのまま任せます。orchestrator.connectRouter()はオーケストレーターをそのルーターに追従させ、interceptLinks()はマイクロアプリ内の通常の<a href>のクリックをそのルーターに送ります。
使うべき場面
- シェルが独自のルーターを持つReactアプリで、一部のルートがマイクロアプリをホストしている。
- マイクロアプリが通常のリンク(Vue、Svelte、AngularまたはHTMLテンプレート)をレンダリングしていて、ページを再読み込みせずにナビゲーションさせたい。
シェルが独自のルーターを持たない場合は、代わりに組み込みのルーターを使います。ルーティングを参照してください。
connectRouter
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
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で作成したデータルーターの場合:
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()を読み取るコンポーネントから橋渡しします。
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の場合:
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を呼び出します。