> For the complete documentation index, see [llms.txt](https://docs.baas.sh/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.baas.sh/baas-sdk/integrations/react.md).

# Use with React

This guide shows a clean way to use the SDK in a React app. It builds on the other guides ([authentication](/baas-sdk/authentication.md), [sessions](/baas-sdk/authentication/sessions.md), and [error handling](/baas-sdk/resources/error-handling.md)) and brings them together.

{% hint style="info" %}
This is an **integration pattern** for the framework-agnostic SDK. There's no separate `@baas/react` package, so copy what you need and adapt it.
{% endhint %}

## How it fits React

`createBaasClient()` is **synchronous**. That makes the integration simple: create the client once as a module-scope singleton, then read the session through a hook. Because `getSession()` is correct on the **first render**, there's no loading state and no flash for an already-signed-in user.

{% stepper %}
{% step %}

### Create the client

A single module-scope instance, shared across your app.

{% code title="baas.ts" %}

```ts
import { createBaasClient } from '@baas/sdk';

// Synchronous: validates config and restores the session from localStorage.
// The network runtime (/networks → Porto) loads lazily, on the first sign-in.
export const baas = createBaasClient({
  projectId:      import.meta.env.VITE_BAAS_PROJECT_ID,
  publishableKey: import.meta.env.VITE_BAAS_PUBLISHABLE_KEY,
  apiUrl:         import.meta.env.VITE_BAAS_API_URL,
});
```

{% endcode %}

Read the three values from your bundler's env: `import.meta.env.VITE_*` for Vite (shown here), or `process.env.NEXT_PUBLIC_*` for Next.js. (On Next.js, see the SSR note below before creating the client.)
{% endstep %}

{% step %}

### Add a provider and hook

The provider seeds state with `getSession()` (sync, available right on the first render) and subscribes to changes. `onAuthStateChange` returns its own unsubscribe function, which doubles as the effect cleanup.

{% code title="BaasProvider.tsx" %}

```tsx
import {
  createContext, useContext, useEffect, useState, type ReactNode,
} from 'react';
import type { BaasSession } from '@baas/sdk';
import { baas } from './baas';

type BaasState = { session: BaasSession | null };

const BaasContext = createContext<BaasState | null>(null);

export function BaasProvider({ children }: { children: ReactNode }) {
  const [session, setSession] = useState<BaasSession | null>(
    () => baas.auth.getSession(),   // correct on the first render — no flash
  );

  useEffect(() => baas.auth.onAuthStateChange(setSession), []);

  return (
    <BaasContext.Provider value={{ session }}>{children}</BaasContext.Provider>
  );
}

export function useBaas(): BaasState {
  const ctx = useContext(BaasContext);
  if (!ctx) throw new Error('useBaas must be used inside <BaasProvider>');
  return ctx;
}
```

{% endcode %}

{% hint style="info" %}
If your linter enforces `react-refresh/only-export-components`, move `useBaas` (and the context) into their own files. The logic is identical.
{% endhint %}
{% endstep %}

{% step %}

### Wrap your app

```tsx
import { BaasProvider } from './BaasProvider';

export function App() {
  return (
    <BaasProvider>
      {/* your routes and components */}
    </BaasProvider>
  );
}
```

{% endstep %}

{% step %}

### Build a sign-in component

Import the `baas` singleton for actions and `useBaas()` for the session. A small `run()` helper sets a `pending` state and catches errors; every button is disabled while a sign-in is in flight (the [debounce](/baas-sdk/authentication.md#avoid-double-prompts) that prevents double passkey prompts).

{% code title="SignIn.tsx" %}

```tsx
import { useState } from 'react';
import type { EIP1193Provider } from '@baas/sdk';
import { baas } from './baas';
import { useBaas } from './BaasProvider';

export function SignIn() {
  const { session } = useBaas();
  const [pending, setPending] = useState(false);
  const [error, setError] = useState<unknown>(null);

  async function run(action: () => Promise<unknown>) {
    setError(null);
    setPending(true);
    try {
      await action();
    } catch (err) {
      setError(err);
    } finally {
      setPending(false);
    }
  }

  function signInWithWallet() {
    const provider = (window as { ethereum?: EIP1193Provider }).ethereum;
    if (!provider) return setError(new Error('No wallet detected.'));
    return run(() => baas.auth.signInWithWallet(provider));
  }

  if (session) return <p>Signed in as {session.address}</p>;

  return (
    <div>
      <button disabled={pending} onClick={() => run(() => baas.auth.signUp())}>
        Create account
      </button>
      <button disabled={pending} onClick={() => run(() => baas.auth.signIn())}>
        Sign in with passkey
      </button>
      <button disabled={pending} onClick={signInWithWallet}>
        Sign in with wallet
      </button>
      {error ? <p role="alert">Sign-in failed. Please try again.</p> : null}
    </div>
  );
}
```

{% endcode %}
{% endstep %}

{% step %}

### Protect routes

With React Router, a guard that reads `session` is all you need:

{% code title="AuthGuard.tsx" %}

```tsx
import { Navigate, Outlet } from 'react-router';
import { useBaas } from './BaasProvider';

export function AuthGuard() {
  const { session } = useBaas();
  if (!session) return <Navigate to="/login" replace />;
  return <Outlet />;
}
```

{% endcode %}
{% endstep %}
{% endstepper %}

## Read and write on-chain

`baas.contract` calls return promises, so they fit any data library you're already using. If you use TanStack Query, here's the standard pattern: `useQuery` for reads, `useMutation` for writes, invalidate on success.

{% code title="hooks/useTotalSupply.ts" %}

```ts
import { useQuery } from '@tanstack/react-query';
import { baas } from './baas';

export function useTotalSupply() {
  return useQuery({
    queryKey: ['ERC20', 'totalSupply'],
    queryFn: () =>
      baas.contract('ERC20').address('0x…')
        .function('totalSupply').read<string>(),
  });
}
```

{% endcode %}

For writes, follow the **simulate → send → wait** pattern: simulate first catches reverts before the wallet popup, and the same `step` builder powers both `simulate()` and `sendTransaction()`.

{% code title="hooks/useMint.ts" %}

```ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { baas } from './baas';
import { useBaas } from './BaasProvider';
import { getWalletSigner } from './getWalletSigner';

export function useMint() {
  const qc = useQueryClient();
  const { session } = useBaas();
  return useMutation({
    mutationFn: async (amount: number) => {
      if (!session) throw new Error('Not signed in');
      const step = baas.contract('ERC20').address('0x…').function('mint').params(amount);
      await step.simulate({ from: session.address }); // preview
      const tx = await step.sendTransaction({ signer: getWalletSigner(session) }); // send
      return tx.wait(); // confirm on-chain
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ['ERC20', 'totalSupply'] }),
  });
}
```

{% endcode %}

A small helper routes the signer based on the sign-in method: pass `window.ethereum` for wallet sessions, return `undefined` for passkey sessions (the SDK handles signing automatically).

{% code title="getWalletSigner.ts" %}

```ts
import type { BaasSession, EIP1193Provider } from '@baas/sdk';

export function getWalletSigner(session: BaasSession): EIP1193Provider | undefined {
  if (session.method === 'wallet') {
    return (window as { ethereum?: EIP1193Provider }).ethereum;
  }
  return undefined; // passkey: let the SDK handle signing automatically
}
```

{% endcode %}

{% hint style="info" %}
**Multi-wallet apps.** This helper assumes the user signed in with the browser-injected wallet (`window.ethereum`, typically MetaMask). If your app also supports WalletConnect, Coinbase Wallet, or wagmi connectors, keep a reference to the exact provider used at `signInWithWallet()` time (in a React context or store) and return that instead.
{% endhint %}

`tx.wait()` resolves only after on-chain confirmation, so `onSuccess` runs when the change is real. Call `qc.invalidateQueries(...)` there to refetch the affected reads.

{% hint style="warning" %}
**Server-side rendering (Next.js).** The SDK reads `localStorage` in its constructor, so it's **browser-only**. The module-scope singleton above works directly in single-page apps (Vite, CRA), but **not** during a server render.

In the Next.js App Router, marking your provider `'use client'` isn't enough, because Client Components are still pre-rendered on the server. Load it client-side only with `next/dynamic` and `{ ssr: false }` (which must itself be called from a Client Component):

```tsx
'use client'; // `ssr: false` is only allowed in a Client Component
import dynamic from 'next/dynamic';

const BaasProvider = dynamic(
  () => import('./BaasProvider').then((m) => m.BaasProvider),
  { ssr: false },
);
```

(Alternatively, create the client inside a `useEffect`.) Never create it at module scope or in a render body on the server.
{% endhint %}

> The same singleton-and-hook pattern generalizes to other frameworks (Vue, Svelte…). Only the syntax changes.

## Next

* [Authentication](/baas-sdk/authentication.md) — the methods wired to the buttons above.
* [Sessions](/baas-sdk/authentication/sessions.md) — what `onAuthStateChange` and the session contain.
* [Error handling](/baas-sdk/resources/error-handling.md) — turn caught errors into friendly messages.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.baas.sh/baas-sdk/integrations/react.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
