mcpbeat Sign in

React Microfrontend Patterns Agent Skill

> Use when building a React frontend that dynamically loads independent bundles sharing a single React instance via import maps, needs frecency-based autocomplete, dynamic schema-driven forms, or Zustand state with localStorage.

2k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
10
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/microsoft/amplifier-bundle-skills --skill react-microfrontend-patterns

The instruction itself

9 sections, as written by the author

React Micro-Frontend Patterns

The Pattern

Problem: You have multiple independently-built UI bundles that must share React at runtime, a form system driven by server-side schemas that change dynamically, and state that needs to persist in the browser and sync across tabs.

Approach: Import maps with Vite's rollupOptions.external for shared React, a useFrecency hook with exponential decay scoring, schema-to-React rendering with action-triggered schema refinement, and Zustand stores with localStorage sync.

Pattern proven in production across multiple React frontends and web services.

Key Design Decisions

1. Import map + rollupOptions.external for shared React

When multiple independently-built bundles run on the same page, each gets its own copy of React. This causes the "dual React instance" bug: hooks break because the React instance that rendered the component is different from the one providing useState.

The fix: externalize React in every bundle's Vite config and provide it via an import map in the HTML:

<!-- index.html -->
<script type="importmap">
{
  "imports": {
    "react": "https://esm.sh/[email protected]",
    "react-dom": "https://esm.sh/[email protected]",
    "react-dom/client": "https://esm.sh/[email protected]/client",
    "react/jsx-runtime": "https://esm.sh/[email protected]/jsx-runtime"
  }
}
</script>
// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      external: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime'],
    },
  },
})

2. The Vite dev-mode problem

Vite's dev server ignores browser import maps — it pre-bundles CJS modules and serves them from /.vite/deps/. This means dynamically-loaded bundles that use bare import React from 'react' will resolve via the import map to a *different* React instance than the host app.

Solutions: Use @vitejs/plugin-react with careful configuration, or write a custom Vite plugin that serves shim modules redirecting bare specifiers to Vite's pre-bundled paths during dev. This is inherently complex — consult your Vite config documentation for the specifics of your setup.

3. useFrecency hook: group-by-suffix, exponential decay scoring

Frecency (frequency + recency) ranks autocomplete results by how often AND how recently a user selected them:

// hooks/useFrecency.ts
const DECAY_HALF_LIFE_MS = 7 * 24 * 60 * 60 * 1000  // 1-week half-life
const STORAGE_KEY = 'frecency-data'
const _suggestionCache = new Map<string, string[]>()  // module-level perf cache

interface FrecencyEntry {
  score: number
  lastUsed: number  // timestamp ms
}

function computeScore(entry: FrecencyEntry, now: number): number {
  const ageFraction = (now - entry.lastUsed) / DECAY_HALF_LIFE_MS
  return entry.score * Math.pow(0.5, ageFraction)
}

function useFrecency(namespace: string) {
  const [entries, setEntries] = useState<Record<string, FrecencyEntry>>(() => {
    const stored = localStorage.getItem(`${STORAGE_KEY}:${namespace}`)
    return stored ? JSON.parse(stored) : {}
  })

  const getSuggestions = useCallback((fieldKey: string, allValues: string[]) => {
    const cacheKey = `${namespace}:${fieldKey}`
    if (_suggestionCache.has(cacheKey)) return _suggestionCache.get(cacheKey)!
    const now = Date.now()
    const sorted = [...allValues].sort((a, b) => {
      const sa = entries[a] ? computeScore(entries[a], now) : 0
      const sb = entries[b] ? computeScore(entries[b], now) : 0
      return sb - sa
    })
    _suggestionCache.set(cacheKey, sorted)
    return sorted
  }, [namespace, entries])

  const recordValue = useCallback((fieldKey: string, value: string) => {
    _suggestionCache.clear()
    setEntries(prev => {
      const now = Date.now()
      const existing = prev[value] ?? { score: 0, lastUsed: now }
      const updated = { ...prev, [value]: { score: existing.score + 1, lastUsed: now } }
      localStorage.setItem(`${STORAGE_KEY}:${namespace}`, JSON.stringify(updated))
      return updated
    })
  }, [namespace])

  const recordAll = useCallback((formData: Record<string, string>) => {
    _suggestionCache.clear()
    setEntries(prev => {
      const now = Date.now()
      const updated = { ...prev }
      for (const value of Object.values(formData)) {
        if (value) {
          const existing = updated[value] ?? { score: 0, lastUsed: now }
          updated[value] = { score: existing.score + 1, lastUsed: now }
        }
      }
      localStorage.setItem(`${STORAGE_KEY}:${namespace}`, JSON.stringify(updated))
      return updated
    })
  }, [namespace])

  return { getSuggestions, recordValue, recordAll }
}

The group-by-suffix pattern extracts a group name from the field key using a regex suffix match — not from URL path splits:

// Group extraction uses /_([a-z]+)$/ on field keys:
function getGroupFromFieldKey(fieldKey: string): string | null {
  const match = fieldKey.match(/_([a-z]+)$/)
  return match ? match[1] : null  // e.g. "repo_name" → "name", "target_branch" → "branch"
}

> Note: The /_([a-z]+)$/ regex extracts groups from field keys like workspace_reporepo. Adapt this regex to your own field naming convention.

4. Zustand store with localStorage sync and Map-based collections

// stores/appStore.ts
import { create } from 'zustand'

interface AppState {
  items: Map<string, Item>
  activeId: string | null
  events: Map<string, AppEvent[]>
  addEvent: (id: string, event: AppEvent) => void
  setItem: (item: Item) => void
}

const MAX_EVENTS_PER_ITEM = 1000  // event capping

const useAppStore = create<AppState>((set) => ({
  items: new Map(),
  activeId: null,
  events: new Map(),

  setItem: (item) => set((state) => {
    const next = new Map(state.items)
    next.set(item.id, item)
    // Persist to localStorage
    localStorage.setItem('items', JSON.stringify([...next.entries()]))
    return { items: next }
  }),

  addEvent: (id, event) => set((state) => {
    const next = new Map(state.events)
    const existing = next.get(id) || []
    // Event capping — keep most recent N events
    const updated = [...existing, event].slice(-MAX_EVENTS_PER_ITEM)
    next.set(id, updated)
    return { events: next }
  }),
}))

The localStorage sync pattern on init:

// Hydrate from localStorage on store creation
const stored = localStorage.getItem('items')
if (stored) {
  try {
    const entries = JSON.parse(stored) as [string, Item][]
    initialItems = new Map(entries)
  } catch {
    // Corrupt localStorage — start fresh
  }
}

Template / Starter Code

// Minimal Zustand store with localStorage persistence
import { create } from 'zustand'

interface AppState {
  items: Record<string, Item>
  setItem: (id: string, item: Item) => void
}

const STORAGE_KEY = 'app-state'

function loadPersistedState(): Record<string, Item> {
  try {
    const raw = localStorage.getItem(STORAGE_KEY)
    return raw ? JSON.parse(raw) : {}
  } catch {
    return {}
  }
}

const useStore = create<AppState>((set) => ({
  items: loadPersistedState(),
  setItem: (id, item) => set((state) => {
    const next = { ...state.items, [id]: item }
    localStorage.setItem(STORAGE_KEY, JSON.stringify(next))
    return { items: next }
  }),
}))

Gotchas & Lessons Learned

  • The dual-React-instance problem is silent until hooks break. Everything renders fine — components appear, props flow. But useState throws "Invalid hook call" because the component was rendered by React instance A but the hook tries to register with React instance B. The error message doesn't mention "two copies of React" — you have to know to look for it.
  • Import maps don't work in Vite dev mode. Vite's dev server pre-bundles dependencies using esbuild, which ignores the browser's import map. A dev shim is required. This is the most common source of "works in production, breaks in dev" issues with shared React.
  • Event capping prevents memory exhaustion. Without the MAX_EVENTS_PER_ITEM cap, a long-running instance would accumulate thousands of events in the Zustand store. The .slice(-N) pattern keeps only the most recent events. Choose N based on what the UI actually renders — if you only show the last 50 events, capping at 1000 is generous.
  • localStorage has a ~5MB limit per origin. Large Map serializations can exceed this. The Zustand persistence should either cap the total size or use IndexedDB for larger datasets. A common symptom: the app works until the localStorage quota is exceeded, then silently stops persisting.

Other skills for the same job

different authors, same section of the catalogue
MCP Builder
by anthropics
vendor ×13

Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).

30k tokens scripts
Changelog Generator
by frostant
×9

Automatically creates user-facing changelogs from git commits by analyzing commit history, categorizing changes, and transforming technical commits into clear, customer-friendly release notes. Turns hours of manual changelog writing into minutes of automated generation.

774 tokens
Finishing A Development Branch
by ZhanlinCui
×7

Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup

1k tokens
MCP Builder
by JayZeeDesign
×7

Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).

37k tokens scripts
Vercel React Native Skills
by vercel-labs
vendor ×6

React Native and Expo best practices for building performant mobile apps. Use when building React Native components, optimizing list performance, implementing animations, or working with native modules. Triggers on tasks involving React Native, Expo, mobile performance, or native platform APIs.

39k tokens
Vercel React Best Practices
by ratacat
×5

React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.

34k tokens
Next Best Practices
by vercel-labs
vendor ×4

Next.js best practices - file conventions, RSC boundaries, data patterns, async APIs, metadata, error handling, route handlers, image/font optimization, bundling

20k tokens
Using Git Worktrees
by ZhanlinCui
×4

Use when starting feature work that needs isolation from current workspace or before executing implementation plans - creates isolated git worktrees with smart directory selection and safety verification

1k tokens

How to use it

Copy the folder

Take microsoft/react-microfrontend-patterns from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.