Cookbook
Forms & Frontend
Client forms with validation, pagination, error handling, and optimistic updates
Client-side patterns for forms, pagination, and error handling.
Form with Validation
Manage form state with plain React state and submit handlers in page/components.
Paginated List
Use useQuery to keep the current page in the URL query string:
// api/users/index.js
export const GET = {
handler: async (ctx) => {
const { db } = ctx;
const page = Math.max(1, parseInt(ctx.req.query.page) || 1);
const limit = Math.min(Math.max(1, parseInt(ctx.req.query.limit) || 20), 100);
const search = ctx.req.query.search || '';
let query = db('users').select('id', 'name', 'email', 'created_at');
if (search) {
query = query.where((qb) => {
qb.where('name', 'like', `%${search}%`).orWhere('email', 'like', `%${search}%`);
});
}
const [users, [{ count }]] = await Promise.all([
query.clone().orderBy('created_at', 'desc').limit(limit).offset((page - 1) * limit),
query.clone().count('* as count'),
]);
return {
data: {
users,
pagination: { page, limit, total: Number(count), totalPages: Math.ceil(Number(count) / limit) },
},
};
},
};// pages/users.jsx
import { useApi, useQuery } from 'arcway/lib/client';
export default function Users() {
const { query, setQuery } = useQuery();
const page = parseInt(query.page) || 1;
const { data, loading } = useApi(`/users?page=${page}&limit=20`);
if (loading) return <p>Loading...</p>;
const { users, pagination } = data;
return (
<div>
<table>
<thead>
<tr><th>Name</th><th>Email</th></tr>
</thead>
<tbody>
{users.map((u) => (
<tr key={u.id}><td>{u.name}</td><td>{u.email}</td></tr>
))}
</tbody>
</table>
<div>
<button disabled={page <= 1} onClick={() => setQuery({ page: page - 1 })}>
Previous
</button>
<span>Page {page} of {pagination.totalPages}</span>
<button disabled={page >= pagination.totalPages} onClick={() => setQuery({ page: page + 1 })}>
Next
</button>
</div>
</div>
);
}Client Error Handling
Handle typed errors from the API using ApiError:
import { useApi, ApiError } from 'arcway/lib/client';
function UserProfile({ id }) {
const { data, error, loading } = useApi(`/users/${id}`);
if (loading) return <p>Loading...</p>;
if (error) {
if (error instanceof ApiError && error.status === 404) {
return <p>User not found</p>;
}
return <p>Something went wrong: {error.message}</p>;
}
return <h1>{data.name}</h1>;
}Mutation with Optimistic Update
Update the UI immediately while the server request is in flight:
// pages/todos.jsx
import { useState } from 'react';
import { useApi } from 'arcway/lib/client';
export default function Todos() {
const { data: todos, loading, mutate } = useApi('/todos');
const [adding, setAdding] = useState(false);
const addTodo = async (text) => {
setAdding(true);
// Optimistic update — add immediately to local state
mutate((current) => [...(current || []), { id: 'temp', text, done: false }]);
try {
const res = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
});
const json = await res.json();
// Replace optimistic entry with the real one from server
mutate((current) => current.map((t) => (t.id === 'temp' ? json.data : t)));
} catch {
// Revert on failure
mutate((current) => current.filter((t) => t.id !== 'temp'));
} finally {
setAdding(false);
}
};
if (loading) return <p>Loading...</p>;
return (
<div>
<ul>{todos.map((t) => <li key={t.id}>{t.text}</li>)}</ul>
<button onClick={() => addTodo('New task')} disabled={adding}>
Add Todo
</button>
</div>
);
}