Recipe · Integration · Vite

Vite SPA integration

Same SDK surface, no Next App Router: providers at the root, lazy-factory encryptor at each call site.

1. Wagmi config#

Vite's environment variables use import.meta.env (with VITE_ prefix to expose them to the browser). WalletConnect needs a project ID; Sepolia RPC needs a transport URL.

src/wagmi.ts
ts
// src/wagmi.ts
import { createConfig, http } from "wagmi";
import { sepolia } from "viem/chains";
import { injected, walletConnect } from "wagmi/connectors";

export const wagmiConfig = createConfig({
  chains: [sepolia],
  connectors: [
    injected(),
    walletConnect({ projectId: import.meta.env.VITE_WC_PROJECT_ID }),
  ],
  transports: {
    [sepolia.id]: http(import.meta.env.VITE_SEPOLIA_RPC_URL),
  },
});

2. Provider tree at the root#

Same shape as Next App Router, WagmiProvider over QueryClientProvider over ZamaProvider. The SDK hooks inside App find everything they need by context.

src/main.tsx
tsx
// src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { WagmiProvider } from "wagmi";
import { sepolia } from "viem/chains";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ZamaProvider, RelayerWeb, indexedDBStorage } from "@zama-fhe/react-sdk";
import { WagmiSigner } from "@zama-fhe/react-sdk/wagmi";
import { wagmiConfig } from "./wagmi";
import { App } from "./App";

const queryClient = new QueryClient();

// ZamaProvider needs a signer, a relayer, and a storage backend.
const signer = new WagmiSigner({ config: wagmiConfig });
const relayer = new RelayerWeb({
  getChainId: () => signer.getChainId(),
  transports: {
    [sepolia.id]: {
      relayerUrl: "https://your-app.com/api/relayer/11155111",
      network: import.meta.env.VITE_SEPOLIA_RPC_URL,
    },
  },
});

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <WagmiProvider config={wagmiConfig}>
      <QueryClientProvider client={queryClient}>
        <ZamaProvider relayer={relayer} signer={signer} storage={indexedDBStorage}>
          <App />
        </ZamaProvider>
      </QueryClientProvider>
    </WagmiProvider>
  </React.StrictMode>,
);

3. Use any hook#

From here, every SDK hook works identically, see the useCreateVesting client-component example in the Next App Router recipe. Lazy-factory encryptor is mandatory in all React hosts.

Optional: 2–3× faster encryption#

Want 2–3× faster encryption? Set threads: 4–8 on Zama's RelayerWeb — this requires cross-origin isolation, because multi-threaded WASM runs on SharedArrayBuffer, which browsers only enable on cross-origin-isolated pages.

src/main.tsx
tsx
const relayer = new RelayerWeb({
  getChainId: () => signer.getChainId(),
  // Opt-in: 4-8 threads is the sweet spot; needs the COOP/COEP
  // headers below. Omit for the default single-threaded worker.
  threads: 8,
  transports: {
    /* unchanged */
  },
});
vite.config.ts
ts
// vite.config.ts — cross-origin isolation for threads > 1
import { defineConfig } from "vite";

const crossOriginIsolation = {
  "Cross-Origin-Opener-Policy": "same-origin",
  "Cross-Origin-Embedder-Policy": "require-corp",
};

export default defineConfig({
  // Covers dev server + vite preview. Mirror the same two headers in
  // your production host config (Vercel, Netlify, nginx, ...).
  server: { headers: crossOriginIsolation },
  preview: { headers: crossOriginIsolation },
});

Two things to know before flipping the switch. Cross-origin isolation is page-wide: Cross-Origin-Embedder-Policy: require-corp blocks cross-origin iframes, scripts, and images that do not send CORP or CORS headers, so audit your embeds first. And the setting fails soft: with threads > 1but no isolation, Zama's SDK logs a warning and encryption keeps working single-threaded — nothing breaks, it is just not faster. React Native is a separate story (no browser worker at all): see the React Native recipe.

See also