Menangani Remote Down & Runtime Error di Micro-Frontend (Rspack Module Federation v1.5)
Di arsitektur micro-frontend dengan Rspack Module Federation v1.5, sering muncul masalah klasik: kalau salah satu remote mati (misalnya…
Menangani Remote Down & Runtime Error di Micro-Frontend (Rspack Module Federation v1.5)
Di arsitektur micro-frontend dengan Rspack Module Federation v1.5, sering muncul masalah klasik:
kalau salah satu remote mati (misalnya profile_app), host (shell_app) ikut crash dengan error:
[Federation Runtime]: remoteEntryExports is undefined
while loading "./store" from webpack/container/reference/profile_app
Hasilnya? Halaman host blank total. Artikel ini akan bahas cara membuat host tetap hidup walaupun remote mati, dengan pola berikut:
- Dynamic import untuk shared store (bukan static import).
- RemoteWrapper: loader dengan retry otomatis + countdown 5 detik.
- ErrorBoundary: isolasi runtime/render error dengan tombol Restart.
Masalah Utama di v1.5
Di v1.5, kalau kita tulis seperti ini di App.tsx host:
import { useUserStore } from "profile_app/store"; // ❌ bahaya
Host akan langsung crash saat profile_app(remote) tidak tersedia, bahkan sebelum React sempat render.
ErrorBoundary pun tidak bisa membantu.
Solusi: dynamic import di useEffect agar error bisa ditangkap dengan .catch().
Solusi Lengkap (Contoh Implementasi)
Repository: https://github.com/azaliafd/rspack-mf-v1.5-v2.0/tree/main/rspack-mf-v1.5
1) RemoteWrapper, Loader + Retry 5 Detik
File: apps/shell-app/src/components/RemoteWrapper.tsx
import React, { useEffect, useState } from "react";
type RemoteWrapperProps = {
name: string;
loader: () => Promise<any>;
fallback?: React.ReactNode;
retrySeconds?: number;
};
export default function RemoteWrapper({
name,
loader,
fallback = <div>Loading...</div>,
retrySeconds = 5,
}: RemoteWrapperProps) {
const [RemoteComp, setRemoteComp] = useState<React.ComponentType | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const [countdown, setCountdown] = useState<number | null>(null);
const [attemptKey, setAttemptKey] = useState(0);
useEffect(() => {
let mounted = true;
setLoading(true);
setError(null);
setRemoteComp(null);
loader()
.then((mod) => {
if (!mounted) return;
const Comp = mod?.default ?? Object.values(mod)[0];
setRemoteComp(() => Comp);
setLoading(false);
setError(null);
})
.catch((err) => {
if (!mounted) return;
setLoading(false);
setError(err as Error);
setCountdown(retrySeconds);
});
return () => {
mounted = false;
};
}, [attemptKey, loader, retrySeconds]);
useEffect(() => {
if (countdown === null) return;
if (countdown <= 0) {
setCountdown(null);
setAttemptKey((k) => k + 1);
return;
}
const t = setTimeout(() => setCountdown((c) => (c ? c - 1 : null)), 1000);
return () => clearTimeout(t);
}, [countdown]);
if (loading) return <>{fallback}</>;
if (RemoteComp) return <RemoteComp />;
return (
<div style={{ border: "1px solid #f0a", padding: 12, borderRadius: 6 }}>
<div>❌ Gagal memuat {name}.</div>
{countdown !== null ? (
<div>🔁 Mencoba ulang dalam {countdown} detik...</div>
) : (
<div style={{ marginTop: 6 }}>
<button onClick={() => setCountdown(retrySeconds)}>
Coba ulang sekarang
</button>
</div>
)}
<div style={{ marginTop: 8, color: "#666", fontSize: 12 }}>
Detail: {String(error?.message ?? error)}
</div>
</div>
);
}
Behavior:
- Kalau gagal import → countdown 5 detik → otomatis retry.
- Ada tombol Coba ulang sekarang buat manual retry.
2) ErrorBoundary — Isolasi Runtime/Render Error
File: apps/shell-app/src/components/ErrorBoundary.tsx
import React from "react";
type Props = {
children: React.ReactNode;
renderFallback?: (reset: () => void, error?: Error) => React.ReactNode;
fallback?: React.ReactNode;
};
type State = { hasError: boolean; error?: Error | null };
export default class ErrorBoundary extends React.Component<Props, State> {
state: State = { hasError: false, error: null };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: any) {
console.error("ErrorBoundary caught:", error, info);
}
reset = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError) {
if (this.props.renderFallback) {
return this.props.renderFallback(this.reset, this.state.error ?? undefined);
}
if (this.props.fallback) return this.props.fallback;
return (
<div style={{ border: "1px solid #e33", padding: 12, borderRadius: 6 }}>
<h3>⚠️ Something went wrong</h3>
<button onClick={this.reset}>Restart component</button>
</div>
);
}
return this.props.children;
}
}
Behavior:
- Kalau komponen crash saat render → fallback muncul.
- Tombol Restart component hanya merender ulang komponen error, bukan reload seluruh halaman.
3) App.tsx — Dynamic Store Import
File: apps/shell-app/src/App.tsx
import React, { useEffect, useState } from "react";
import RemoteWrapper from "./components/RemoteWrapper";
import ErrorBoundary from "./components/ErrorBoundary";
const App: React.FC = () => {
const [age, setAge] = useState<number | null>(null);
useEffect(() => {
let mounted = true;
import("profile_app/store")
.then((mod) => {
if (!mounted) return;
const useStore = mod.useUserStore;
setAge(useStore.getState().age);
const unsub = useStore.subscribe((s: any) => {
if (mounted) setAge(s.age);
});
return () => unsub();
})
.catch((err) => {
console.warn("Tidak bisa load profile_app/store:", err?.message ?? err);
if (mounted) setAge(-1); // tanda "N/A"
});
return () => {
mounted = false;
};
}, []);
return (
<div style={{ padding: 16, fontFamily: "sans-serif" }}>
<h1> Shell Application (Host)</h1>
<p>
Current user age from shared store:{" "}
<strong>{age === null ? "Loading…" : age === -1 ? "N/A" : age}</strong>
</p>
<h2>Profile Card</h2>
<ErrorBoundary
renderFallback={(reset) => (
<div style={{ padding: 8, border: "1px solid #f66", borderRadius: 6 }}>
ProfileCard error saat render.
<div style={{ marginTop: 8 }}>
<button onClick={reset}>Restart component</button>
</div>
</div>
)}
>
<RemoteWrapper
name="ProfileCard"
loader={() => import("profile_app/ProfileCard")}
fallback={<div>Loading ProfileCard…</div>}
retrySeconds={5}
/>
</ErrorBoundary>
<h2>Dashboard Panel</h2>
<ErrorBoundary
renderFallback={(reset) => (
<div style={{ padding: 8, border: "1px solid #f66", borderRadius: 6 }}>
DashboardPanel error saat render.
<div style={{ marginTop: 8 }}>
<button onClick={reset}>Restart component</button>
</div>
</div>
)}
>
<RemoteWrapper
name="DashboardPanel"
loader={() => import("dashboard_app/DashboardPanel")}
fallback={<div>Loading DashboardPanel…</div>}
retrySeconds={5}
/>
</ErrorBoundary>
</div>
);
};
export default App;
Cara Cek Behavior
- Jalankan semua app:
pnpm --filter profile-app dev
pnpm --filter dashboard-app dev
pnpm --filter shell-app dev
2. Scenario A: semua remote hidup
- ProfileCard & DashboardPanel muncul.
- Klik “Increase Age” → host ikut update.
3. Scenario B: matikan profile-app
- Host tidak crash.
- Muncul fallback:
❌ Gagal memuat ProfileCard.
🔁 Mencoba ulang dalam 5 detik...
[Coba ulang sekarang]
- Saat remote hidup lagi → otomatis retry → ProfileCard muncul.
4. Scenario C: klik “Crash this component”
- Fallback ErrorBoundary muncul.
- Klik Restart component → komponen hidup lagi tanpa mengganggu container lain.
메타데이터
- post_id
- 92650731a77c
- slug
- menangani-remote-down-runtime-error-di-micro-frontend-rspack-module-federation-v1-5-92650731a77c
- url
- https://medium.com/@azaliafd/menangani-remote-down-runtime-error-di-micro-frontend-rspack-module-federation-v1-5-92650731a77c
- canonical_url
- https://medium.com/@azaliafd/menangani-remote-down-runtime-error-di-micro-frontend-rspack-module-federation-v1-5-92650731a77c
- author_url
- https://medium.com/@azaliafd
- status
- ok
- fetched_at
- 2026-07-17 13:33:47