Skip to main content

Getting Started with @ductape/react

The @ductape/react package provides React hooks and components for building real-time applications with Ductape. Built on top of @ductape/client, it offers a React-idiomatic way to work with databases, storage, workflows, and more.

Installation

npm install @ductape/react @ductape/client
# or
yarn add @ductape/react @ductape/client
# or
pnpm add @ductape/react @ductape/client
info

Both @ductape/react and @ductape/client are required. The React package is a peer dependency wrapper around the core client.

Requirements

  • React 17.0.0 or higher
  • TypeScript 4.5+ (optional but recommended)

Basic Setup

Frontend: use only publishable key

In browser/frontend never use accessKey. Use publishableKey and baseUrl (your Ductape proxy). Get your publishable key from Workbench → Tokens → Publishable Key.

Session token from your backend (publishable key)

The design is: a long-lived publishable key (safe in the frontend) and a short-lived session token per user (created on your backend). Your backend calls ductape.sessions.start() with your access key (e.g. at user login) and returns the session token to the client. The frontend then includes that token as session in every Ductape request. Session hooks (useSessionStart, useSessionVerify, etc.) are not available with a publishable key—sessions are created only on the backend. See Sessions (backend) and Session Hooks.

1. Create a config (publishable key, product, env)

Use a long-lived publishable key in your frontend config. The session token is not part of this config—it comes from your backend (e.g. login response) and must be passed in every request (step 2).

// config.ts
export const baseUrl = import.meta.env.VITE_BASE_URL || 'https://api.ductape.app';
export const publishableKey = import.meta.env.VITE_PUBLISHABLE_KEY || '';
export const product = import.meta.env.VITE_PRODUCT || 'ductape:rematch';
export const env = import.meta.env.VITE_ENV || 'snd';

2. Wrap Your App with DuctapeProvider

import { DuctapeProvider } from '@ductape/react';
import { publishableKey, product, env } from './config';

function App() {
return (
<DuctapeProvider
config={{
publishableKey,
product,
env,
}}
autoConnect={false}
>
<YourApp />
</DuctapeProvider>
);
}

export default App;

3. Get the session token from your backend and pass it to every request

Your backend creates a session (e.g. at login) with the access key and returns the token to the frontend:

// Backend (e.g. login handler) — use @ductape/sdk with accessKey
const ductape = new Ductape({ accessKey: process.env.DUCTAPE_ACCESS_KEY });
const { token } = await ductape.sessions.start({
product: 'your-product',
env: 'prd',
tag: 'user-session',
data: { userId: user.id, email: user.email },
});
// Return { token } to the client in the login response

The frontend receives that token (e.g. from your auth state or API) and passes it as session in every Ductape request:

// Frontend: get session from your auth (e.g. login response / auth context)
const sessionToken = useAuth().sessionToken; // or from state/context after login

4. Use Hooks in Your Components (include session in every request)

import { useDatabaseQuery } from '@ductape/react';
import { useAuth } from './auth'; // your auth context that holds the session token from backend

function UsersList() {
const { sessionToken } = useAuth();
const { data, isLoading, error, refetch } = useDatabaseQuery(
'users',
{
table: 'users',
where: { active: true },
limit: 10,
session: sessionToken,
}
);

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

return (
<div>
<button onClick={() => refetch()}>Refresh</button>
<ul>
{(data?.data ?? []).map((user: any) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}

Provider Configuration

The DuctapeProvider accepts these props:

interface DuctapeProviderProps {
config: {
publishableKey: string; // From Workbench → Tokens → Publishable Key
product?: string;
env?: string;
};
autoConnect?: boolean; // Auto-connect WebSocket (default: false)
children: React.ReactNode;
}

Session is not in the provider config. Pass the session token (from your backend) in the options for every hook call (e.g. useDatabaseQuery, useActionRun, useBrokerPublish, useMutation).

Environment Variables

Vite: use the VITE_ prefix so variables are exposed to the client. Only the publishable key and app config belong here. The session token must come from your backend (e.g. after login), not from env.

# .env (Vite)
VITE_PUBLISHABLE_KEY=dpk_xxx
VITE_PRODUCT=ductape:rematch
VITE_ENV=snd
// config.ts
export const publishableKey = import.meta.env.VITE_PUBLISHABLE_KEY || '';
export const product = import.meta.env.VITE_PRODUCT || 'ductape:rematch';
export const env = import.meta.env.VITE_ENV || 'snd';

For local development only, you may use a dev session token in env (e.g. VITE_SESSION=...) if your backend is not running; in production the session must always come from your backend.

Create React App: use REACT_APP_ prefix and process.env.REACT_APP_PUBLISHABLE_KEY, etc.

Quick Examples

In the examples below, sessionToken is the session token your frontend received from your backend (e.g. in the login response). Pass it in every request when using a publishable key.

Query Data

import { useDatabaseQuery } from '@ductape/react';
import { useAuth } from './auth'; // or wherever you store the backend-issued session token

function ProductList() {
const { sessionToken } = useAuth();
const { data, isLoading, error } = useDatabaseQuery(
'products',
{
table: 'products',
where: { category: 'electronics' },
orderBy: [{ column: 'price', order: 'asc' }],
limit: 20,
session: sessionToken,
}
);

if (isLoading) return <div>Loading products...</div>;
if (error) return <div>Error loading products</div>;

return (
<div>
{(data?.data ?? []).map((product: any) => (
<div key={product.id}>
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
))}
</div>
);
}

Insert Data

import { useDatabaseInsert } from '@ductape/react';
import { useState } from 'react';
import { useAuth } from './auth';

function CreateUser() {
const { sessionToken } = useAuth();
const [name, setName] = useState('');
const [email, setEmail] = useState('');

const { mutate, isLoading, error } = useDatabaseInsert({
onSuccess: (data) => {
console.log('User created:', data);
setName('');
setEmail('');
}
});

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
mutate({
table: 'users',
data: { name, email },
session: sessionToken,
});
};

return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
required
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
type="email"
required
/>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Creating...' : 'Create User'}
</button>
{error && <p>Error: {error.message}</p>}
</form>
);
}

Real-time Subscription

import { useDatabaseSubscription } from '@ductape/react';
import { useState } from 'react';
import { useAuth } from './auth';

function LiveMessages() {
const { sessionToken } = useAuth();
const [messages, setMessages] = useState([]);

useDatabaseSubscription({
table: 'messages',
where: { channel: 'general' },
session: sessionToken,
onChange: (event) => {
if (event.type === 'insert') {
setMessages(prev => [...prev, event.data.new]);
}
}
});

return (
<ul>
{messages.map(msg => (
<li key={msg.id}>{msg.text}</li>
))}
</ul>
);
}

File Upload

import { useUpload } from '@ductape/react';
import { useAuth } from './auth';

function FileUploader() {
const { sessionToken } = useAuth();
const { upload, progress, isLoading, error } = useUpload();

const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
upload({
storage: 'gcp-storage',
fileName: `uploads/documents/${file.name}`,
data: file,
mimeType: file.type || 'application/octet-stream',
session: sessionToken,
});
}
};

return (
<div>
<input
type="file"
onChange={handleFileChange}
disabled={isLoading}
/>
{isLoading && <p>Uploading: {progress?.percentage}%</p>}
{error && <p>Error: {error.message}</p>}
</div>
);
}

Execute Workflow

import { useWorkflowExecute } from '@ductape/react';
import { useAuth } from './auth';

function ProcessOrder({ orderId }: { orderId: string }) {
const { sessionToken } = useAuth();
const { mutate, isLoading, data, error } = useWorkflowExecute();

const handleProcess = () => {
mutate({
workflow: 'process-order',
input: { orderId },
session: sessionToken,
});
};

return (
<div>
<button onClick={handleProcess} disabled={isLoading}>
{isLoading ? 'Processing...' : 'Process Order'}
</button>
{data && <p>Execution ID: {data.executionId}</p>}
{error && <p>Error: {error.message}</p>}
</div>
);
}

Accessing the Client Directly

You can access the underlying Ductape client instance. Include the session token (from your backend) in every call when using a publishable key.

import { useDuctape } from '@ductape/react';
import { useAuth } from './auth';

function MyComponent() {
const { sessionToken } = useAuth();
const { client, isReady, isConnected } = useDuctape();

const handleCustomOperation = async () => {
const result = await client.databases.query({
table: 'custom_table',
session: sessionToken,
});
};

return (
<div>
<p>Ready: {isReady ? 'Yes' : 'No'}</p>
<p>Connected: {isConnected ? 'Yes' : 'No'}</p>
</div>
);
}

TypeScript Support

The hooks are fully typed. You can provide type parameters:

import { useAuth } from './auth';

interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}

function UsersList() {
const { sessionToken } = useAuth();
const { data, isLoading } = useDatabaseQuery<User>(
'users',
{
table: 'users',
limit: 10,
session: sessionToken,
}
);

// data.data is typed as User[]
return (
<ul>
{(data?.data ?? []).map(user => (
<li key={user.id}>
{user.name} - {user.role}
</li>
))}
</ul>
);
}

Error Boundaries

Wrap your components with error boundaries to handle errors gracefully:

import { ErrorBoundary } from 'react-error-boundary';

function App() {
return (
<DuctapeProvider config={...}>
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<YourApp />
</ErrorBoundary>
</DuctapeProvider>
);
}

Best Practices

  1. Use query keys wisely: The first parameter to hooks like useDatabaseQuery is a query key used for caching. Make it unique per query.

  2. Memoize query options: Use useMemo for complex query options to prevent unnecessary re-renders:

import { useAuth } from './auth';

function UsersList() {
const { sessionToken } = useAuth();
const queryOptions = useMemo(
() => ({
table: 'users',
where: { active: true },
session: sessionToken,
}),
[sessionToken]
);
const { data } = useDatabaseQuery('users', queryOptions);
// ...
}
  1. Handle loading and error states: Always provide UI feedback for loading and error states.

  2. Cleanup subscriptions: Subscriptions automatically cleanup when components unmount.

  3. Use TypeScript: Leverage TypeScript for type safety and better developer experience.

Next Steps