| name | frontend-pitfalls |
| description | Critical pitfalls and safety rules for Miden frontend development. Covers WASM initialization, concurrent access crashes, COOP/COEP headers, BigInt handling, Bech32 network mismatches, IndexedDB state loss, auto-sync side effects, Vite configuration, and React rendering race conditions. Use when reviewing, debugging, or writing Miden frontend code. |
Miden Frontend Pitfalls
FP1: WASM Initialization Race (CRITICAL)
Components that use Miden hooks before MidenProvider finishes WASM initialization will crash.
function App() {
const { accounts } = useAccounts();
return <div>{accounts.length}</div>;
}
<MidenProvider
config={{ rpcUrl: "testnet" }}
loadingComponent={<p>Loading WASM...</p>}
>
<App />
</MidenProvider>
function App() {
const { isReady } = useMiden();
if (!isReady) return <p>Loading...</p>;
return <WalletView />;
}
FP2: Recursive WASM Access Crash (CRITICAL)
The WASM client is single-threaded. Concurrent calls crash with "recursive use of an object detected".
const handleClick = async () => {
sync();
await send({ ... });
};
const client = useMidenClient();
const { runExclusive } = useMiden();
await runExclusive(async () => {
await client.syncState();
});
Built-in hooks (useSend, useConsume, etc.) already use runExclusive internally. This pitfall applies when using useMidenClient() directly or mixing manual client calls with hook mutations.
FP3: COOP/COEP Headers Missing (CRITICAL)
WASM SharedArrayBuffer requires these headers. Without them, WASM init silently fails.
The template's source-of-truth pattern is to opt into COOP/COEP via the Vite plugin explicitly:
import { midenVitePlugin } from "@miden-sdk/vite-plugin";
export default defineConfig({
plugins: [react(), midenVitePlugin({ crossOriginIsolation: true })],
});
Do not rely on the plugin's own default — @miden-sdk/vite-plugin defaults crossOriginIsolation to false, and that default has shifted across releases. Pass true explicitly on any route that runs Miden client code.
COOP/COEP must also be set on the production server — the plugin only covers the Vite dev server. See vite-wasm-setup for per-host configs (Nginx, Vercel, Cloudflare).
Gotcha (opt-out path): Cross-origin-isolation breaks third-party iframes, external scripts without CORS, and OAuth popups. If a route must host those and cannot use Miden client code, either (a) use Cross-Origin-Embedder-Policy: credentialless for weaker isolation that still allows most cross-origin resources, or (b) scope crossOriginIsolation: false to that specific route and accept that Miden operations won't work there. Do not disable isolation globally as a convenience.
FP4: BigInt Type Mismatch (HIGH)
All token amounts in the SDK are bigint. Passing number causes TypeScript errors or runtime failures.
await send({ from, to, assetId, amount: 1000 });
await createFaucet({ maxSupply: 1000000, ... });
await send({ from, to, assetId, amount: 1000n });
await createFaucet({ maxSupply: BigInt(1000000), ... });
import { parseAssetAmount } from "@miden-sdk/react";
const amount = parseAssetAmount(inputValue, 8);
Gotcha: JSON.stringify cannot serialize bigint. Use a custom replacer or convert to string first.
FP5: Bech32 Network Mismatch (HIGH)
Bech32-encoded account IDs include the network. A devnet address on testnet points to a different or nonexistent account.
const ADMIN = "miden1qy35...";
const ADMIN = "0x1234567890abcdef";
account.id().toBech32(networkId, accountInterface);
Both hex and bech32 formats work in all hooks. Prefer hex for constants, bech32 for display.
FP6: Auto-Sync Side Effects (MEDIUM)
Default autoSyncInterval is 15000ms (15 seconds). Each sync triggers re-renders in useAccounts, useAccount, useNotes, etc.
<MidenProvider config={{ rpcUrl: "testnet" }}>
<SendForm /> {}
</MidenProvider>
const MemoizedForm = React.memo(SendForm);
<MidenProvider config={{ rpcUrl: "testnet", autoSyncInterval: 0 }}>
FP7: IndexedDB State Loss (MEDIUM)
The client persists accounts, keys, and notes in IndexedDB. Browser "Clear site data", private browsing, or storage pressure can delete everything.
- Warn users that clearing browser data deletes their wallet
- Consider external signers (Para, Turnkey) for production — keys are server-side
- Implement account export/backup for local keystore users
FP8: Vite Configuration Requirements (MEDIUM)
The @miden-sdk/vite-plugin package handles all Miden-specific Vite config. The template's source-of-truth pattern — which you should copy for any new Miden app — is:
import { midenVitePlugin } from "@miden-sdk/vite-plugin";
export default defineConfig({
plugins: [react(), midenVitePlugin({ crossOriginIsolation: true })],
});
midenVitePlugin() handles WASM loading, top-level await, pre-bundling exclusion, and — when crossOriginIsolation: true is passed — emits the COOP same-origin + COEP require-corp headers the SDK requires for SharedArrayBuffer.
| Option | Plugin default | Template default | Purpose |
|---|
crossOriginIsolation | false | true | Emit COOP/COEP headers for SharedArrayBuffer |
Always pass crossOriginIsolation: true explicitly. The plugin's false default is wrong for a Miden app, and relying on it risks silent WASM-init failures if the default shifts in a future release. The opt-out path (routes that host OAuth popups or cross-origin iframes incompatible with isolation) is discussed in FP3. For production, set the same headers at the server level — see vite-wasm-setup for host-specific configs.
FP9: React StrictMode Double-Init (LOW)
React 19 StrictMode double-invokes effects in development. MidenProvider handles this via isInitializedRef, but direct WasmWebClient.createClient() calls will initialize twice. (WasmWebClient is the low-level WASM client class; for most apps use MidenProvider / MidenClient instead.)
useEffect(() => {
const client = await WasmWebClient.createClient(url);
}, []);
<MidenProvider config={{ rpcUrl: "testnet" }}>
Quick Reference
| # | Pitfall | Severity | One-Line Rule |
|---|
| FP1 | WASM init race | CRITICAL | Use loadingComponent or check isReady |
| FP2 | Recursive WASM | CRITICAL | Use runExclusive() for all direct client access |
| FP3 | COOP/COEP | CRITICAL | Add headers in vite.config.ts AND production server |
| FP4 | BigInt | HIGH | All amounts are bigint (1000n not 1000) |
| FP5 | Bech32 mismatch | HIGH | Match network in rpcUrl and addresses |
| FP6 | Auto-sync | MEDIUM | Set autoSyncInterval: 0 if UI stability matters |
| FP7 | IndexedDB loss | MEDIUM | Warn users; use external signers for production |
| FP8 | Vite config | MEDIUM | Always pass midenVitePlugin({ crossOriginIsolation: true }) — don't rely on the plugin default |
| FP9 | StrictMode | LOW | Use MidenProvider, not manual client creation |