Mit React starten

Dein erster durchgehender Datenfluss: ein Schema, zwei Backend-Funktionen und eine React-Komponente, die bei Änderungen automatisch mitzieht.

TypeScript. Vom Backend bis zum Button.

Baue eine kleine Aufgabenliste mit echten Live-Updates.

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.

SchemaBeschreibt deine Daten
FunktionenLesen & schreiben
ReactBleibt automatisch aktuell

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.

omega/schema.ts
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.

omega/tasks.ts
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.

src/main.tsx
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.

src/App.tsx
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.

Im Projekt prüfen
npm run typecheck
npm test
npm run build
© 2026 GalaxumGebaut für das, was du vorhast.