Frontend Integration
This guide covers how to integrate Cobalt with your frontend application, with a focus on React Router 7.
SDK Setup
The generated SDK needs to be initialized before use:
typescript
import sdk from "sdk";
sdk.init({
// Configuration options
});React Router 7 Integration
Basic Setup
typescript
// root.tsx
import sdk from "sdk";
sdk.init({
// Default configuration
});
export default function Root() {
return (
<html>
<body>
<Outlet />
</body>
</html>
);
}With Authentication
typescript
// root.tsx
import { redirect } from "react-router";
import sdk from "sdk";
import {
makeAuthLoader,
accessTokenFromCookie,
} from "@cobalt27/auth/react/rr7";
// Initialize with cookie-based auth
sdk.init({
auth: accessTokenFromCookie,
});
// Create the auth loader
export const loader = makeAuthLoader(
{
clientID: "my-app",
issuer: "http://localhost:4000",
unprotectedPaths: ["/login", "/error", "/logout"],
},
(tokens) => {
// Re-initialize with fresh tokens
sdk.init({
auth: tokens.tokens.access,
});
},
(error) => {
return redirect("/error?error=" + error);
}
);
export default function Root() {
return (
<html>
<body>
<Outlet />
</body>
</html>
);
}Data Fetching
Basic Query
typescript
// routes/users.tsx
import sdk from "sdk";
export const loader = async () => {
const users = await sdk.query.users();
return { users };
};
export default function Users() {
const { users } = useLoaderData<typeof loader>();
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}With SWR
typescript
import useSWR from "swr";
import sdk, { _ } from "sdk";
const getTodos = sdk.query.todos(_)().$lazy;
export default function TodoList() {
const [search, setSearch] = useState("");
const { data: todos, mutate } = useSWR(
["todos", search],
() => getTodos({
where: {
text: { contains: search || undefined }
}
})
);
return (
<div>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search..."
/>
<ul>
{todos?.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
}With React Query
typescript
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import sdk, { _ } from "sdk";
const getUser = sdk.query.user({ id: _ })().$lazy;
const updateUser = sdk.mutation.updateUser(_)().$lazy;
export default function UserProfile({ userId }) {
const queryClient = useQueryClient();
const { data: user, isLoading } = useQuery({
queryKey: ["user", userId],
queryFn: () => getUser({ id: userId })
});
const mutation = useMutation({
mutationFn: (data) => updateUser({ id: userId, data }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["user", userId] });
}
});
if (isLoading) return <div>Loading...</div>;
return (
<div>
<h1>{user.name}</h1>
<button onClick={() => mutation.mutate({ name: "New Name" })}>
Update
</button>
</div>
);
}Mutations
Basic Mutation
typescript
import sdk from "sdk";
const createTodo = sdk.mutation.createOneTodo(_)().$lazy;
export default function CreateTodoForm() {
const [text, setText] = useState("");
const handleSubmit = async (e) => {
e.preventDefault();
await createTodo({
data: { text, completed: false }
});
setText("");
// Refresh data...
};
return (
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button type="submit">Add</button>
</form>
);
}Optimistic Updates
typescript
import useSWR, { useSWRConfig } from "swr";
const updateTodo = sdk.mutation.updateOneTodo(_)().$lazy;
function TodoItem({ todo }) {
const { mutate } = useSWRConfig();
const toggleComplete = async () => {
// Optimistically update
mutate(
["todos"],
(current) => current.map(t =>
t.id === todo.id
? { ...t, completed: !t.completed }
: t
),
{ revalidate: false }
);
// Actually update
await updateTodo({
where: { id: todo.id },
data: { completed: !todo.completed }
});
// Revalidate
mutate(["todos"]);
};
return (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={toggleComplete}
/>
{todo.text}
</li>
);
}Subscriptions
Custom Hook
typescript
// hooks/useAsyncIterable.tsx
import { useEffect, useRef } from "react";
export function useAsyncIterable<T>(
getIterable: () => Promise<AsyncIterable<T>>,
onValue: (value: T) => void,
onComplete: () => void,
deps: any[] = []
) {
useEffect(() => {
let cancelled = false;
(async () => {
const iterable = await getIterable();
const iterator = iterable[Symbol.asyncIterator]();
while (!cancelled) {
const { value, done } = await iterator.next();
if (done || cancelled) break;
onValue(value);
}
if (!cancelled) onComplete();
})();
return () => {
cancelled = true;
};
}, deps);
}Using the Hook
typescript
import { useAsyncIterable } from "@/hooks/useAsyncIterable";
import sdk, { _ } from "sdk";
const streamTodos = sdk.subscription.streamTodos(_)().$lazy;
export default function RealtimeTodos() {
const [todos, setTodos] = useState<Todo[]>([]);
const [newTodos, setNewTodos] = useState<Todo[]>([]);
// Fetch initial todos
useEffect(() => {
sdk.query.todos()().then(setTodos);
}, []);
// Subscribe to new todos
useAsyncIterable(
async () => await streamTodos({ where: {} }),
(todo) => {
setNewTodos(prev => [...prev, todo]);
},
() => console.log("Stream ended")
);
const allTodos = [...todos, ...newTodos];
return (
<ul>
{allTodos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}Error Handling
Global Error Boundary
typescript
// routes/error.tsx
import { useRouteError } from "react-router";
export default function ErrorPage() {
const error = useRouteError();
return (
<div>
<h1>Something went wrong</h1>
<p>{error instanceof Error ? error.message : "Unknown error"}</p>
</div>
);
}Operation-Level Errors
typescript
const handleCreate = async () => {
try {
await createTodo({ data: { text } });
} catch (error) {
if (error instanceof Error) {
setError(error.message);
}
}
};Authentication Routes
Login Page
typescript
// routes/login.tsx
export default function Login() {
// OpenAuthJS handles the login flow
// The auth loader in root.tsx will redirect to the
// OpenAuth login page automatically
return (
<div>
<h1>Redirecting to login...</h1>
</div>
);
}Logout Route
typescript
// routes/logout.tsx
import { redirect } from "react-router";
export const loader = async () => {
// Clear auth cookies and redirect
return redirect("/login", {
headers: {
"Set-Cookie": [
"access_token=; Path=/; HttpOnly; Max-Age=0",
"refresh_token=; Path=/; HttpOnly; Max-Age=0"
].join(", ")
}
});
};
export default function Logout() {
return <div>Logging out...</div>;
}Complete Example
Here's a complete Todo app frontend:
typescript
// routes/_index/route.tsx
import sdk, { _, type TodoWithBy } from "sdk";
import { useState, useMemo } from "react";
import { useAsyncIterable } from "@/hooks/useAsyncGen";
import useSWR from "swr";
import { Link } from "react-router-dom";
// Define lazy operations
const getTodos = sdk.query.todos(_)().$lazy;
const createTodo = sdk.mutation.createOneTodo(_)().$lazy;
const updateTodo = sdk.mutation.updateOneTodo(_)().$lazy;
const deleteTodo = sdk.mutation.deleteOneTodo(_)().$lazy;
const streamTodos = sdk.subscription.streamTodos(_)().$lazy;
export default function Index() {
const [searchText, setSearchText] = useState("");
const [todoText, setTodoText] = useState("");
// Fetch todos with SWR
const { data: todos, mutate: mutateTodos } = useSWR(
["todos", searchText],
() => getTodos({
where: {
text: { contains: searchText || undefined }
}
})
);
// Handle new todo
const handleAddTodo = async () => {
if (!todoText.trim()) return;
await createTodo({
data: { text: todoText, completed: false }
});
setTodoText("");
mutateTodos();
};
// Stream new todos from other users
const [streamedTodos, setStreamedTodos] = useState<TodoWithBy[]>([]);
useAsyncIterable(
async () => await streamTodos({ where: {} }),
(todo) => setStreamedTodos(t => [...t, todo]),
() => {}
);
// Combine fetched and streamed todos
const allTodos = useMemo(
() => [...(todos ?? []), ...streamedTodos],
[todos, streamedTodos]
);
return (
<div>
<Link to="/logout">Logout</Link>
<h1>Todo App</h1>
<input
placeholder="Search..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
<input
placeholder="New todo..."
value={todoText}
onChange={(e) => setTodoText(e.target.value)}
/>
<button onClick={handleAddTodo}>Add</button>
<ul>
{allTodos?.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onUpdate={mutateTodos}
/>
))}
</ul>
</div>
);
}Next Steps
- SDK Usage — Master the generated SDK
- The $lazy Pattern — Reusable operations
- Examples — Complete example app