Skip to main content

What is Hydration?

Hydration is the process of:
  1. Server: Render React component to HTML string
  2. Client: Load React runtime and attach to existing DOM
  3. Result: Interactive React components
Server: <Component /> → HTML String

Client: HTML + React Runtime → Interactive UI

Enabling Hydration

Set hydrate: true in your UI config:
import { MyReactWidget } from './widgets/my-widget';

@Tool({
  name: 'my_tool',
  ui: {
    template: MyReactWidget,
    hydrate: true, // Enable client-side hydration
  },
})

How It Works

When hydrate: true:
  1. The React component is rendered server-side with renderToString()
  2. The HTML includes hydration markers
  3. The React runtime is bundled with the widget
  4. On the client, hydrateRoot() attaches to the existing DOM
<!-- Server-rendered HTML with hydration markers -->
<div id="root" data-reactroot="">
  <div class="widget">
    <h1>Hello</h1>
    <button>Click me</button>
  </div>
</div>

<!-- React runtime for hydration -->
<script>
  // React attaches to existing DOM
  hydrateRoot(document.getElementById('root'), <MyWidget {...props} />);
</script>

Interactive React Components

With hydration, you can use full React features:
src/widgets/counter.tsx
import { useState } from 'react';
import { TemplateContext } from '@frontmcp/ui';

interface CounterOutput {
  initialCount: number;
}

export function CounterWidget({ output }: TemplateContext<unknown, CounterOutput>) {
  const [count, setCount] = useState(output.initialCount);

  return (
    <div className="p-4 bg-white rounded-lg shadow">
      <h2 className="text-xl font-bold mb-4">Counter</h2>
      <div className="flex items-center gap-4">
        <button
          onClick={() => setCount(c => c - 1)}
          className="px-4 py-2 bg-gray-200 rounded"
        >
          -
        </button>
        <span className="text-2xl font-mono">{count}</span>
        <button
          onClick={() => setCount(c => c + 1)}
          className="px-4 py-2 bg-blue-500 text-white rounded"
        >
          +
        </button>
      </div>
    </div>
  );
}
@Tool({
  name: 'counter',
  ui: {
    template: CounterWidget,
    hydrate: true,
  },
})

Using Hooks

With hydration enabled, React hooks work as expected:
import { useState, useEffect, useCallback } from 'react';

export function DataWidget({ output }: TemplateContext<unknown, DataOutput>) {
  const [data, setData] = useState(output.initialData);
  const [loading, setLoading] = useState(false);

  const refresh = useCallback(async () => {
    setLoading(true);
    try {
      const result = await window.mcpBridge?.callTool('refresh_data', {});
      setData(result);
    } finally {
      setLoading(false);
    }
  }, []);

  useEffect(() => {
    // Set up polling
    const interval = setInterval(refresh, 30000);
    return () => clearInterval(interval);
  }, [refresh]);

  return (
    <div>
      {loading && <div>Loading...</div>}
      <DataDisplay data={data} />
      <button onClick={refresh}>Refresh</button>
    </div>
  );
}

MCP Bridge Integration

Access MCP Bridge in hydrated components:
import { useCallback } from 'react';

export function ToolInvoker() {
  const callTool = useCallback(async (name: string, params: object) => {
    if (window.mcpBridge?.callTool) {
      return await window.mcpBridge.callTool(name, params);
    }
    throw new Error('MCP Bridge not available');
  }, []);

  const handleClick = async () => {
    const result = await callTool('my_tool', { param: 'value' });
    console.log('Tool result:', result);
  };

  return <button onClick={handleClick}>Call Tool</button>;
}

Custom Hook for MCP Bridge

src/widgets/hooks/use-mcp-bridge.ts
import { useEffect, useState, useCallback } from 'react';
import type { MCPBridge, HostContext } from '@frontmcp/ui';

export function useMcpBridge() {
  const [bridge, setBridge] = useState<MCPBridge | null>(null);
  const [context, setContext] = useState<HostContext | null>(null);

  useEffect(() => {
    if (window.mcpBridge) {
      setBridge(window.mcpBridge);
      setContext(window.mcpBridge.context);

      // Subscribe to context changes
      const unsubscribe = window.mcpBridge.onContextChange(setContext);
      return unsubscribe;
    }
  }, []);

  const callTool = useCallback(async (name: string, params: object) => {
    if (!bridge?.callTool) throw new Error('callTool not available');
    return bridge.callTool(name, params);
  }, [bridge]);

  return { bridge, context, callTool };
}
Usage:
function MyWidget() {
  const { context, callTool } = useMcpBridge();

  return (
    <div className={context?.theme === 'dark' ? 'dark' : ''}>
      <button onClick={() => callTool('action', {})}>
        Do Something
      </button>
    </div>
  );
}

Platform Considerations

Hydration requires:
  • JavaScript execution on the client
  • React runtime to be loaded

OpenAI

Full support for hydration with React runtime loading from CDN.

Claude

Limited support. Claude Artifacts have restricted JavaScript execution. Consider:
  • Keep interactions simple
  • Use HTMX instead for basic interactivity
  • Test thoroughly in Claude environment

Gemini

Basic JavaScript support. May have limitations on external script loading.

Bundle Size

Hydration adds the React runtime to your widget. Consider:
ui: {
  template: MyWidget,
  hydrate: true,
  // Only hydrate on platforms that support it
  servingMode: 'inline',
}

Minimizing Bundle Size

  1. Use smaller alternatives - Preact instead of React
  2. Code split - Only load what’s needed
  3. Lazy load - Import components dynamically

When to Use Hydration

Use hydration when:
  • Complex client-side state management
  • Real-time updates (polling, WebSocket)
  • Rich form interactions
  • Animations and transitions
  • Reusable interactive components
Don’t use hydration when:
  • Static data display only
  • Simple interactions (use HTMX)
  • Claude/Gemini is primary target
  • Bundle size is a concern

Alternative: HTMX

For simpler interactions, consider HTMX instead:
import { button } from '@frontmcp/ui';

// No hydration needed
const html = button('Load More', {
  htmx: {
    get: '/api/items?page=2',
    target: '#items',
    swap: 'beforeend',
  },
});
HTMX provides server-driven interactivity without a JavaScript framework.

Best Practices

  1. Check platform support - Not all platforms handle hydration well
  2. Minimize bundle size - Only include what you need
  3. Handle SSR/CSR differences - Avoid hydration mismatches
  4. Use progressive enhancement - Work without JS first
  5. Test across platforms - Verify behavior everywhere