Skip to content

React Integration preview

The @hermes/react package provides React hooks that wrap the Hermes bridge, giving you a familiar data-fetching pattern for calling C# methods from React components.

Installation

bash
npm install @hermes/bridge @hermes/react

useInvoke Hook

useInvoke is the primary hook for calling C# methods. It manages loading state, error handling, and result caching — similar to hooks from libraries like SWR or React Query.

tsx
import { useInvoke } from '@hermes/react';

function RuntimeInfo() {
  const { data, loading, error } = useInvoke<string>('getRuntime');

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return <p>Running on {data}</p>;
}

Auto-Invoke on Mount

By default, useInvoke calls the C# method immediately when the component mounts (with no arguments). This works well for methods that don't require parameters:

tsx
const { data } = useInvoke<string>('getPlatform');
// data is populated automatically after mount

Manual Invoke with Arguments

For methods that take arguments, use the returned invoke function:

tsx
import { useState } from 'react';
import { useInvoke } from '@hermes/react';

function GreetCard() {
  const [name, setName] = useState('');
  const { data, loading, invoke } = useInvoke<string>('greet');

  const handleGreet = async () => {
    await invoke(name);
  };

  return (
    <div>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
        placeholder="Enter your name"
      />
      <button onClick={handleGreet} disabled={loading}>
        {loading ? 'Greeting...' : 'Greet'}
      </button>
      {data && <p>{data}</p>}
    </div>
  );
}

Refetching

Call refetch() to re-invoke the method with the same arguments as the last call:

tsx
const { data, refetch } = useInvoke<SystemInfo>('getSystemInfo');

return (
  <div>
    <pre>{JSON.stringify(data, null, 2)}</pre>
    <button onClick={refetch}>Refresh</button>
  </div>
);

API Reference

typescript
function useInvoke<TResult>(method: string): UseInvokeResult<TResult>;

interface UseInvokeResult<TResult> {
  data: TResult | null;      // Last successful result
  loading: boolean;           // True while a call is in flight
  error: Error | null;        // Last error, or null
  invoke: (...args: unknown[]) => Promise<TResult>;  // Manual invoke
  refetch: () => Promise<TResult>;                    // Re-invoke with last args
}

Using the Bridge Directly

For event subscriptions or cases where the hook pattern doesn't fit, use the bridge directly:

tsx
import { useEffect } from 'react';
import { bridge } from '@hermes/bridge';

function DownloadProgress() {
  const [progress, setProgress] = useState(0);

  useEffect(() => {
    const unsubscribe = bridge.on<{ percent: number }>('download-progress', (data) => {
      setProgress(data.percent);
    });
    return unsubscribe;
  }, []);

  return <progress value={progress} max={100} />;
}

Complete Example

Here's a full React + Hermes.Web application.

Program.cs (C# host):

csharp
using Hermes;
using Hermes.Web;

HermesWindow.Prewarm();

var builder = HermesWebAppBuilder.Create(args);

builder.ConfigureWindow(opts =>
{
    opts.Title = "React Desktop App";
    opts.Width = 800;
    opts.Height = 600;
    opts.DevToolsEnabled = true;
});

#if DEBUG
builder.UseDevServer("http://localhost:5176");
#else
builder.UseStaticFiles("frontend/dist");
builder.UseSpaFallback();
#endif

builder.UseInteropBridge(bridge =>
{
    bridge.Register<string, string>("greet", name => $"Hello from C#, {name}!");
    bridge.Register("getRuntime", () => $".NET {Environment.Version}");
    bridge.Register("getPlatform", () => Environment.OSVersion.Platform.ToString());
});

var app = builder.Build();
app.Run();

App.tsx (React frontend):

tsx
import { useInvoke } from '@hermes/react';
import { useState } from 'react';

export default function App() {
  const [name, setName] = useState('');
  const runtime = useInvoke<string>('getRuntime');
  const platform = useInvoke<string>('getPlatform');
  const greeter = useInvoke<string>('greet');

  return (
    <main>
      <h1>Hermes + React</h1>

      <p>
        {runtime.loading ? 'Loading...' : `${runtime.data} on ${platform.data}`}
      </p>

      <input
        value={name}
        onChange={e => setName(e.target.value)}
        placeholder="Your name"
      />
      <button onClick={() => greeter.invoke(name)}>
        Greet
      </button>

      {greeter.data && <p>{greeter.data}</p>}
    </main>
  );
}

Next Steps