Skip to main content

Database Hooks

React hooks for working with Ductape databases. These hooks provide automatic loading states, error handling, and React-specific optimizations.

useDatabase

Connect to a database and get the connection state.

import { useDatabase } from '@ductape/react';

function MyComponent() {
const { isConnected, connect, disconnect, error } = useDatabase();

useEffect(() => {
connect({ database: 'main' });
}, []);

if (error) return <div>Error connecting: {error.message}</div>;
if (!isConnected) return <div>Connecting...</div>;

return <div>Connected to database</div>;
}

useDatabaseQuery

Query data from a database table with automatic loading and error states.

Basic Query

import { useDatabaseQuery } from '@ductape/react';

function UsersList() {
const { data, isLoading, error, refetch } = useDatabaseQuery(
'users', // Query key
{
table: 'users',
limit: 10
}
);

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

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

Query with Filters

function ActiveUsers() {
const { data, isLoading } = useDatabaseQuery(
['users', 'active'],
{
table: 'users',
where: { active: true, role: 'user' },
orderBy: [{ column: 'createdAt', order: 'desc' }],
limit: 20
}
);

if (isLoading) return <div>Loading...</div>;

return (
<div>
<h2>Active Users ({data?.count})</h2>
<ul>
{data?.rows.map(user => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
</div>
);
}

Reactive Query with Dependencies

function ProductList() {
const [category, setCategory] = useState('electronics');
const [priceLimit, setPriceLimit] = useState(1000);

const { data, isLoading } = useDatabaseQuery(
['products', category, priceLimit],
{
table: 'products',
where: {
category: category,
price: { $lt: priceLimit }
},
orderBy: [{ column: 'price', order: 'asc' }]
}
);

return (
<div>
<select value={category} onChange={(e) => setCategory(e.target.value)}>
<option value="electronics">Electronics</option>
<option value="books">Books</option>
<option value="clothing">Clothing</option>
</select>

<input
type="number"
value={priceLimit}
onChange={(e) => setPriceLimit(Number(e.target.value))}
placeholder="Max price"
/>

{isLoading ? (
<div>Loading...</div>
) : (
<div>
{data?.rows.map(product => (
<div key={product.id}>
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
))}
</div>
)}
</div>
);
}

With TypeScript

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

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

if (isLoading) return <div>Loading...</div>;

return (
<ul>
{data?.rows.map(user => (
<li key={user.id}>
{user.name} ({user.role})
</li>
))}
</ul>
);
}

useDatabaseInsert

Insert data into a database table.

Basic Insert

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

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

const { mutate, isLoading, error } = useDatabaseInsert({
onSuccess: (data) => {
console.log('User created:', data.rows[0]);
setName('');
setEmail('');
alert('User created successfully!');
},
onError: (err) => {
alert('Failed to create user: ' + err.message);
}
});

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

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 className="error">{error.message}</p>}
</form>
);
}

Bulk Insert

function BulkUserImport() {
const { mutate, isLoading } = useDatabaseInsert({
onSuccess: (data) => {
alert(`Imported ${data.rows.length} users`);
}
});

const handleImport = () => {
const users = [
{ name: 'Alice', email: 'alice@example.com' },
{ name: 'Bob', email: 'bob@example.com' },
{ name: 'Charlie', email: 'charlie@example.com' }
];

mutate({
table: 'users',
data: users
});
};

return (
<button onClick={handleImport} disabled={isLoading}>
{isLoading ? 'Importing...' : 'Import Users'}
</button>
);
}

With Optimistic Updates

function CreateTodo() {
const [text, setText] = useState('');

const { mutate } = useDatabaseInsert({
onMutate: async (variables) => {
// Optimistically add to UI
const newTodo = {
id: `temp-${Date.now()}`,
text: variables.data.text,
completed: false
};

// Return context for rollback
return { newTodo };
},
onSuccess: (data, variables, context) => {
console.log('Todo created:', data.rows[0]);
},
onError: (error, variables, context) => {
// Rollback optimistic update
console.error('Failed, rolling back:', error);
}
});

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
mutate({
table: 'todos',
data: { text, completed: false }
});
setText('');
};

return (
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="New todo"
/>
<button type="submit">Add</button>
</form>
);
}

useDatabaseUpdate

Update existing records in a database table.

import { useDatabaseUpdate } from '@ductape/react';

function UpdateUserForm({ userId }: { userId: string }) {
const [name, setName] = useState('');

const { mutate, isLoading, error } = useDatabaseUpdate({
onSuccess: () => {
alert('User updated!');
}
});

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
mutate({
table: 'users',
where: { id: userId },
data: { name, updatedAt: new Date() }
});
};

return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="New name"
/>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Updating...' : 'Update'}
</button>
{error && <p>{error.message}</p>}
</form>
);
}

Toggle Todo Example

function TodoItem({ todo }: { todo: any }) {
const { mutate } = useDatabaseUpdate({
onSuccess: () => {
console.log('Todo updated');
}
});

const handleToggle = () => {
mutate({
table: 'todos',
where: { id: todo.id },
data: { completed: !todo.completed }
});
};

return (
<div>
<input
type="checkbox"
checked={todo.completed}
onChange={handleToggle}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
</div>
);
}

useDatabaseDelete

Delete records from a database table.

import { useDatabaseDelete } from '@ductape/react';

function DeleteUserButton({ userId }: { userId: string }) {
const { mutate, isLoading } = useDatabaseDelete({
onSuccess: () => {
alert('User deleted');
}
});

const handleDelete = () => {
if (confirm('Are you sure?')) {
mutate({
table: 'users',
where: { id: userId }
});
}
};

return (
<button onClick={handleDelete} disabled={isLoading}>
{isLoading ? 'Deleting...' : 'Delete'}
</button>
);
}

Bulk Delete

function DeleteCompleted() {
const { mutate, isLoading } = useDatabaseDelete({
onSuccess: (result) => {
alert(`Deleted ${result.count} completed todos`);
}
});

const handleDeleteCompleted = () => {
if (confirm('Delete all completed todos?')) {
mutate({
table: 'todos',
where: { completed: true }
});
}
};

return (
<button onClick={handleDeleteCompleted} disabled={isLoading}>
{isLoading ? 'Deleting...' : 'Delete Completed'}
</button>
);
}

useDatabaseSubscription

Subscribe to real-time database changes.

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

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

useDatabaseSubscription({
table: 'messages',
where: { channel: 'general' },
onChange: (event) => {
if (event.type === 'insert') {
setMessages(prev => [...prev, event.data.new]);
} else if (event.type === 'update') {
setMessages(prev =>
prev.map(msg =>
msg.id === event.data.new.id ? event.data.new : msg
)
);
} else if (event.type === 'delete') {
setMessages(prev =>
prev.filter(msg => msg.id !== event.data.old.id)
);
}
}
});

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

Subscription with Initial Data

function RealtimeTodoList() {
const { data: initialData, isLoading } = useDatabaseQuery(
'todos',
{
table: 'todos',
where: { completed: false }
}
);

const [todos, setTodos] = useState([]);

// Initialize with query data
useEffect(() => {
if (initialData) {
setTodos(initialData.rows);
}
}, [initialData]);

// Subscribe to changes
useDatabaseSubscription({
table: 'todos',
where: { completed: false },
onChange: (event) => {
if (event.type === 'insert') {
setTodos(prev => [...prev, event.data.new]);
}
}
});

if (isLoading) return <div>Loading...</div>;

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

Complete CRUD Example

interface Todo {
id: string;
text: string;
completed: boolean;
}

function TodoApp() {
const [newTodoText, setNewTodoText] = useState('');

// Query todos
const { data, isLoading, refetch } = useDatabaseQuery<Todo>(
'todos',
{
table: 'todos',
orderBy: [{ column: 'createdAt', order: 'desc' }]
}
);

// Create mutation
const { mutate: createTodo } = useDatabaseInsert({
onSuccess: () => {
refetch();
setNewTodoText('');
}
});

// Update mutation
const { mutate: updateTodo } = useDatabaseUpdate({
onSuccess: () => refetch()
});

// Delete mutation
const { mutate: deleteTodo } = useDatabaseDelete({
onSuccess: () => refetch()
});

const handleCreate = (e: React.FormEvent) => {
e.preventDefault();
createTodo({
table: 'todos',
data: { text: newTodoText, completed: false }
});
};

const handleToggle = (todo: Todo) => {
updateTodo({
table: 'todos',
where: { id: todo.id },
data: { completed: !todo.completed }
});
};

const handleDelete = (todoId: string) => {
if (confirm('Delete this todo?')) {
deleteTodo({
table: 'todos',
where: { id: todoId }
});
}
};

if (isLoading) return <div>Loading todos...</div>;

return (
<div>
<form onSubmit={handleCreate}>
<input
value={newTodoText}
onChange={(e) => setNewTodoText(e.target.value)}
placeholder="New todo"
required
/>
<button type="submit">Add</button>
</form>

<ul>
{data?.rows.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggle(todo)}
/>
<span
style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}
>
{todo.text}
</span>
<button onClick={() => handleDelete(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}

Next Steps