Mit React starten
Dein erster durchgehender Datenfluss: ein Schema, zwei Backend-Funktionen und eine React-Komponente, die bei Änderungen automatisch mitzieht.
Alles bereit?
Dieser Guide setzt das lokale React-Projekt aus der Installation voraus. Das SDK, React und TypeScript sind dort bereits eingerichtet. Lass npm run dev im Projekt laufen.
Du ersetzt omega/schema.ts, omega/tasks.ts und src/main.tsx durch die folgenden Beispiele und legst zusätzlich src/App.tsx an. Das Schema entspricht dem Starter; vorhandene Aufgaben können erhalten bleiben.
1 Beschreibe deine Daten
Eine Aufgabe hat einen Titel und einen Erledigt-Status. Mit .owner("owner") erhält sie außerdem einen Eigentümer. Die Zugriffsregel wird vom nativen Host durchgesetzt.
import { defineSchema, defineTable, v } from "@ndb/client/server";
export default defineSchema({
tasks: defineTable({
title: v.string(),
done: v.boolean(),
})
.owner("owner")
.index("done"),
});Validatoren wie v.string() erzeugen TypeScript-Typen und prüfen Werte zur Laufzeit. Der Index auf done steht für passende Abfragen bereit.
2 Schreibe deine Backend-Funktionen
Eine Query liest Daten. Eine Mutation verändert sie in einer Transaktion. Beide verwenden die Typen deines Schemas.
import { query, mutation } from "./_generated/server";
import { v } from "@ndb/client/server";
export const list = query({
args: {},
handler: (ctx) => ctx.db.query({ table: "tasks" }),
});
export const create = mutation({
args: { title: v.string() },
returns: v.id("tasks"),
handler: (ctx, { title }) => {
if (!title.trim()) throw new Error("Bitte einen Titel eingeben.");
return ctx.db.insert("tasks", {
title: title.trim(),
done: false,
owner: ctx.auth.id,
});
},
});list liefert die für die aktuelle Identität sichtbaren Aufgaben. create prüft den Titel, setzt den Eigentümer aus dem authentifizierten Kontext und liefert die neue ID zurück.
3 Verbinde React mit deinem Backend
Der OmegaProvider stellt den Client für deine Komponenten bereit. Dieses vollständige Beispiel lädt die lokale Demo-Identität vom Dev-Server, übernimmt den generierten Branch und schließt den Client beim Entfernen der Komponente.
import { useEffect, useMemo, useState } from "react";
import { createRoot } from "react-dom/client";
import { createClient } from "@ndb/client";
import { OmegaProvider, OmegaErrorBoundary } from "@ndb/client/react";
import { branch } from "../omega/_generated/environment";
import App from "./App";
import "./style.css";
function ConnectedApp({ token }: { token: string }) {
const client = useMemo(
() => createClient({ url: location.origin, token, branch }),
[token],
);
useEffect(() => () => client.close(), [client]);
return (
<OmegaProvider client={client}>
<OmegaErrorBoundary
fallback={({ error, retry }) => (
<div role="alert">
<p>{error.message}</p>
<button onClick={retry}>Erneut versuchen</button>
</div>
)}
>
<App />
</OmegaErrorBoundary>
</OmegaProvider>
);
}
function Bootstrap() {
const [token, setToken] = useState<string>();
const [error, setError] = useState("");
useEffect(() => {
const abort = new AbortController();
// Lokale Demo-Identität vom Dev-Server, kein Produktions-Login.
fetch("/__omega/client-config", { signal: abort.signal })
.then(async (response) => {
if (!response.ok) throw new Error("Verbindung fehlgeschlagen.");
return response.json();
})
.then((config) => setToken(config.tokens.alice))
.catch((cause) => {
if (!abort.signal.aborted) setError(String(cause));
});
return () => abort.abort();
}, []);
if (error) return <p role="alert">{error}</p>;
if (!token) return <p>Verbindung wird hergestellt …</p>;
return <ConnectedApp token={token} />;
}
createRoot(document.getElementById("root")!).render(<Bootstrap />);Der Error Boundary zeigt Query-Fehler an und bietet einen erneuten Versuch. Du musst keine Backend-Implementierungen in den Browser importieren.
4 Lies und verändere deine Daten
Lege src/App.tsx an. useQuery abonniert das Ergebnis deiner Funktion. Mit useMutationState erhältst du zusätzlich den Speicherstatus und mögliche Fehler.
import { useState, type FormEvent } from "react";
import { useQuery, useMutationState } from "@ndb/client/react";
import { api } from "../omega/_generated/api";
export default function App() {
const tasks = useQuery(api.tasks.list);
const create = useMutationState(api.tasks.create);
const [title, setTitle] = useState("");
async function addTask(event: FormEvent) {
event.preventDefault();
try {
await create.run({ title });
setTitle("");
} catch {
// Der Hook stellt den Fehler unten als create.error bereit.
}
}
return (
<main>
<h1>Meine Aufgaben</h1>
<form onSubmit={addTask}>
<input
aria-label="Neue Aufgabe"
value={title}
onChange={(event) => setTitle(event.target.value)}
placeholder="Was hast du vor?"
/>
<button disabled={create.pending || !title.trim()}>
{create.pending ? "Speichert …" : "Hinzufügen"}
</button>
</form>
{create.error && <p role="alert">{create.error.message}</p>}
{tasks === undefined ? (
<p>Aufgaben werden geladen …</p>
) : tasks.length === 0 ? (
<p>Deine erste Aufgabe wartet auf dich.</p>
) : (
<ul>{tasks.map((task) => <li key={task.id}>{task.title}</li>)}</ul>
)}
</main>
);
}tasks === undefined bedeutet, dass das erste Ergebnis noch aussteht. Ein leeres Array bedeutet, dass noch keine Aufgaben vorhanden sind. Sobald eine Änderung bestätigt und in den aktiven Live-Queries sichtbar ist, wird create.run() abgeschlossen.
5 Probiere die Live-Updates aus
Öffne deine lokale App in zwei Browserfenstern. Beide verwenden in diesem Beispiel dieselbe Demo-Identität. Erstelle in einem Fenster eine Aufgabe – die Liste im anderen aktualisiert sich automatisch.
Die React-Hooks teilen sich eine WebSocket-Verbindung. Änderungen an gelesenen Daten lösen eine erneute Ausführung betroffener Queries aus. Du brauchst für diese Liste keinen eigenen Polling-Timer und keinen manuellen Refetch.
npm run typecheck
npm test
npm run build