← Back to list

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…

Azalia Fathimah Dinah · 2025-09-14 10:03 · 0 claps · 3.4 min read
#micro-frontends #react #error-handling #web-development #indonesia
Open on Medium ↗
Wiki topics: 🌐 · Web Development

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:

  1. Dynamic import untuk shared store (bukan static import).
  2. RemoteWrapper: loader dengan retry otomatis + countdown 5 detik.
  3. 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

  1. 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