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
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
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.
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 — 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.