- {run.deps.random.next()} - {app.appOwner.id}
+
+
Account
+
+ Todos are stored in local SQLite. When you sync across devices, your
+ data is end-to-end encrypted using your mnemonic.
+
+
+
+
{
+ setShowMnemonic(!showMnemonic);
+ }}
+ className="w-full"
+ />
+
+ {showMnemonic && appOwner.mnemonic && (
+
+
+ Your Mnemonic (keep this safe!)
+
+
+
+ )}
+
+
+
+
+
+
+
);
};
-// // Creates a typed React Hook for accessing Evolu from EvoluProvider context.
-// // You can also use `evolu` directly, but the hook enables replacing Evolu
-// // in tests via the EvoluProvider.
-// const useEvolu = createUseEvolu(evolu);
-
-// /**
-// * Subscribe to Evolu errors (database, network, sync issues). These should not
-// * happen in normal operation, so log them for debugging. Show users a friendly
-// * error message instead of technical details.
-// */
-// evolu.subscribeError(() => {
-// const error = evolu.getError();
-// if (!error) return;
-
-// alert("🚨 Evolu error occurred! Check the console.");
-// // eslint-disable-next-line no-console
-// console.error(error);
-// });
-
-// export const EvoluMinimalExample: FC = () => (
-//
-//
-//
-//
-// Minimal Todo App
-//
-//
-
-//
-// {/*
-// Suspense delivers great UX (no loading flickers) and DX (no loading
-// states to manage). Highly recommended with Evolu.
-// */}
-//
-//
-//
-//
-//
-//
-//
-// );
-
-// // Extract the row type from the query for type-safe component props.
-// type TodosRow = typeof todosQuery.Row;
-
-// const Todos: FC = () => {
-// // useQuery returns live data - component re-renders when data changes.
-// const todos = useQuery(todosQuery);
-// const { insert } = useEvolu();
-// const [newTodoTitle, setNewTodoTitle] = useState("");
-
-// const addTodo = () => {
-// const result = insert(
-// "todo",
-// {
-// title: newTodoTitle.trim(),
-// },
-// {
-// onComplete: () => {
-// setNewTodoTitle("");
-// },
-// },
-// );
-
-// if (!result.ok) {
-// alert(formatTypeError(result.error));
-// }
-// };
-
-// return (
-//
-//
-// {todos.map((todo) => (
-//
-// ))}
-//
-
-//
-// {
-// setNewTodoTitle(e.target.value);
-// }}
-// onKeyDown={(e) => {
-// if (e.key === "Enter") addTodo();
-// }}
-// placeholder="Add a new todo..."
-// className="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6"
-// />
-//
-//
-//
-// );
-// };
-
-// const TodoItem: FC<{
-// row: TodosRow;
-// }> = ({ row: { id, title, isCompleted } }) => {
-// const { update } = useEvolu();
-
-// const handleToggleCompletedClick = () => {
-// update("todo", {
-// id,
-// isCompleted: Evolu.booleanToSqliteBoolean(!isCompleted),
-// });
-// };
-
-// const handleRenameClick = () => {
-// const newTitle = window.prompt("Edit todo", title);
-// if (newTitle == null) return;
-
-// const result = update("todo", { id, title: newTitle });
-// if (!result.ok) {
-// alert(formatTypeError(result.error));
-// }
-// };
-
-// const handleDeleteClick = () => {
-// update("todo", {
-// id,
-// // Soft delete with isDeleted flag (CRDT-friendly, preserves sync history).
-// isDeleted: Evolu.sqliteTrue,
-// });
-// };
-
-// return (
-//
-//
-//
-//
-// {title}
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-// );
-// };
-
-// const OwnerActions: FC = () => {
-// const evolu = useEvolu();
-// const appOwner = use(evolu.appOwner);
-
-// const [showMnemonic, setShowMnemonic] = useState(false);
-
-// // Restore owner from mnemonic to sync data across devices.
-// const handleRestoreAppOwnerClick = () => {
-// const mnemonic = window.prompt("Enter your mnemonic to restore your data:");
-// if (mnemonic == null) return;
-
-// const result = Evolu.Mnemonic.from(mnemonic.trim());
-// if (!result.ok) {
-// alert(formatTypeError(result.error));
-// return;
-// }
-
-// // void evolu.restoreAppOwner(result.value);
-// };
-
-// const handleResetAppOwnerClick = () => {
-// if (confirm("Are you sure? This will delete all your local data.")) {
-// // void evolu.resetAppOwner();
-// }
-// };
-
-// const handleDownloadDatabaseClick = () => {
-// void evolu.exportDatabase().then((data) => {
-// using objectUrl = Evolu.createObjectURL(
-// new Blob([data], { type: "application/x-sqlite3" }),
-// );
-
-// const link = document.createElement("a");
-// link.href = objectUrl.url;
-// link.download = `${evolu.name}.sqlite3`;
-// link.click();
-// });
-// };
-
-// return (
-//
-//
Account
-//
-// Todos are stored in local SQLite. When you sync across devices, your
-// data is end-to-end encrypted using your mnemonic.
-//
-
-//
-//
{
-// setShowMnemonic(!showMnemonic);
-// }}
-// className="w-full"
-// />
-
-// {showMnemonic && appOwner.mnemonic && (
-//
-//
-// Your Mnemonic (keep this safe!)
-//
-//
-//
-// )}
-
-//
-//
-//
-//
-//
-//
-//
-// );
-// };
-
-// const Button: FC<{
-// title: string;
-// className?: string;
-// onClick: () => void;
-// variant?: "primary" | "secondary";
-// }> = ({ title, className, onClick, variant = "secondary" }) => {
-// const baseClasses =
-// "px-3 py-2 text-sm font-medium rounded-lg transition-colors";
-// const variantClasses =
-// variant === "primary"
-// ? "bg-blue-600 text-white hover:bg-blue-700"
-// : "bg-gray-100 text-gray-700 hover:bg-gray-200";
-
-// return (
-//
-// {title}
-//
-// );
-// };
-
-// /**
-// * Formats Evolu Type errors into user-friendly messages.
-// *
-// * Evolu Type typed errors ensure every error type used in schema must have a
-// * formatter. TypeScript enforces this at compile-time, preventing unhandled
-// * validation errors from reaching users.
-// *
-// * The `createFormatTypeError` function handles both built-in and custom errors,
-// * and lets us override default formatting for specific errors.
-// *
-// * Click on `createFormatTypeError` below to see how to write your own
-// * formatter.
-// */
-// const formatTypeError = Evolu.createFormatTypeError<
-// Evolu.MinLengthError | Evolu.MaxLengthError
-// >((error): string => {
-// switch (error.type) {
-// case "MinLength":
-// return `Text must be at least ${error.min} character${error.min === 1 ? "" : "s"} long`;
-// case "MaxLength":
-// return `Text is too long (maximum ${error.max} characters)`;
-// }
-// });
+const Button: FC<{
+ title: string;
+ className?: string;
+ onClick: () => void;
+ variant?: "primary" | "secondary";
+}> = ({ title, className, onClick, variant = "secondary" }) => {
+ const baseClasses =
+ "px-3 py-2 text-sm font-medium rounded-lg transition-colors";
+ const variantClasses =
+ variant === "primary"
+ ? "bg-blue-600 text-white hover:bg-blue-700"
+ : "bg-gray-100 text-gray-700 hover:bg-gray-200";
+
+ return (
+
+ {title}
+
+ );
+};
+
+/**
+ * Formats Evolu Type errors into user-friendly messages.
+ *
+ * Evolu Type typed errors ensure every error type used in schema must have a
+ * formatter. TypeScript enforces this at compile-time, preventing unhandled
+ * validation errors from reaching users.
+ *
+ * The `createFormatTypeError` function handles both built-in and custom errors,
+ * and lets us override default formatting for specific errors.
+ *
+ * Click on `createFormatTypeError` below to see how to write your own
+ * formatter.
+ */
+const formatTypeError = Evolu.createFormatTypeError<
+ Evolu.MinLengthError | Evolu.MaxLengthError
+>((error): string => {
+ switch (error.type) {
+ case "MinLength":
+ return `Text must be at least ${error.min} character${error.min === 1 ? "" : "s"} long`;
+ case "MaxLength":
+ return `Text is too long (maximum ${error.max} characters)`;
+ }
+});
diff --git a/biome_errors.txt b/biome_errors.txt
deleted file mode 100644
index 2490138b5..000000000
--- a/biome_errors.txt
+++ /dev/null
@@ -1,276 +0,0 @@
-apps/web/src/components/SectionProvider.tsx:164:10 lint/correctness/useHookAtTopLevel ━━━━━━━━━━
-
- × This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
-
- 162 │ }
- 163 │ // eslint-disable-next-line react-hooks/rules-of-hooks
- > 164 │ return useStore(store, selector);
- │ ^^^^^^^^
- 165 │ };
- 166 │
-
- i Hooks should not be called after an early return.
-
- 158 │ ): T => {
- 159 │ const store = useContext(SectionStoreContext);
- > 160 │ if (!store) {
- │
- > 161 │ return {} as T;
- │ ^^^^^^^^^^^^^^^
- 162 │ }
- 163 │ // eslint-disable-next-line react-hooks/rules-of-hooks
-
- i For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
-
- i See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
-
-
-packages/common/test/Function.test.ts:3:8 lint/style/useImportType FIXABLE ━━━━━━━━━━
-
- × Some named imports are only used as types.
-
- 1 │ import { describe, expect, expectTypeOf, test } from "vitest";
- 2 │ import type { NonEmptyArray, NonEmptyReadonlyArray } from "../src/Array.js";
- > 3 │ import {
- │ ^
- > 4 │ exhaustiveCheck,
- ...
- > 12 │ todo,
- > 13 │ } from "../src/Function.js";
- │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
- 14 │ import type { ReadonlyRecord } from "../src/Object.js";
- 15 │
-
- i This import is only used as a type.
-
- 7 │ lazyNull,
- 8 │ lazyTrue,
- > 9 │ lazyUndefined,
- │ ^^^^^^^^^^^^^
- 10 │ lazyVoid,
- 11 │ readonly,
-
- i This import is only used as a type.
-
- 8 │ lazyTrue,
- 9 │ lazyUndefined,
- > 10 │ lazyVoid,
- │ ^^^^^^^^
- 11 │ readonly,
- 12 │ todo,
-
- i Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules.
-
- i Safe fix: Add inline type keywords.
-
- 7 7 │ lazyNull,
- 8 8 │ lazyTrue,
- 9 │ - ··lazyUndefined,
- 10 │ - ··lazyVoid,
- 9 │ + ··type·lazyUndefined,
- 10 │ + ··type·lazyVoid,
- 11 11 │ readonly,
- 12 12 │ todo,
-
-
-packages/react/src/useOwner.ts:16:12 lint/correctness/useHookAtTopLevel ━━━━━━━━━━━━━━━
-
- × This hook is being called from a nested function, but all hooks must be called unconditionally from the top-level component.
-
- 14 │ useEffect(() => {
- 15 │ if (owner == null) return;
- > 16 │ return evolu.useOwner(owner);
- │ ^^^^^^^^^^^^^^
- 17 │ }, [evolu, owner]);
- 18 │ };
-
- i For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
-
- i See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
-
-
-packages/react/src/useQueries.ts:50:5 lint/correctness/useHookAtTopLevel ━━━━━━━━━━━━━━
-
- × This hook is being called from a nested function, but all hooks must be called unconditionally from the top-level component.
-
- 48 │ // Safe until the number of queries is stable.
- 49 │ // eslint-disable-next-line react-hooks/rules-of-hooks
- > 50 │ useQuerySubscription(query, { once: i > queries.length - 1 }),
- │ ^^^^^^^^^^^^^^^^^^^^
- 51 │ ) as never;
- 52 │ };
-
- i For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
-
- i See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
-
-
-packages/react/src/useQuerySubscription.ts:29:5 lint/correctness/useHookAtTopLevel ━━━━━━━━━━
-
- × This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
-
- 27 │ if (once) {
- 28 │ /* eslint-disable react-hooks/rules-of-hooks */
- > 29 │ useEffect(
- │ ^^^^^^^^^
- 30 │ // No useSyncExternalStore, no unnecessary updates.
- 31 │ () => evolu.subscribeQuery(query)(lazyVoid),
-
- i For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
-
- i See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
-
-
-packages/react/src/useQuerySubscription.ts:38:5 lint/correctness/useHookAtTopLevel ━━━━━━━━━━
-
- × This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
-
- 37 │ return useSyncExternalStore(
- > 38 │ useMemo(() => evolu.subscribeQuery(query), [evolu, query]),
- │ ^^^^^^^
- 39 │ useMemo(() => () => evolu.getQueryRows(query), [evolu, query]),
- 40 │ () => emptyRows as QueryRows
,
-
- i Hooks should not be called after an early return.
-
- 31 │ () => evolu.subscribeQuery(query)(lazyVoid),
- 32 │ [evolu, query],
- > 33 │ );
- │
- > 34 │ return evolu.getQueryRows(query);
- │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- 35 │ }
- 36 │
-
- i For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
-
- i See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
-
-
-packages/react/src/useQuerySubscription.ts:39:5 lint/correctness/useHookAtTopLevel ━━━━━━━━━━
-
- × This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
-
- 37 │ return useSyncExternalStore(
- 38 │ useMemo(() => evolu.subscribeQuery(query), [evolu, query]),
- > 39 │ useMemo(() => () => evolu.getQueryRows(query), [evolu, query]),
- │ ^^^^^^^
- 40 │ () => emptyRows as QueryRows,
- 41 │ /* eslint-enable react-hooks/rules-of-hooks */
-
- i Hooks should not be called after an early return.
-
- 31 │ () => evolu.subscribeQuery(query)(lazyVoid),
- 32 │ [evolu, query],
- > 33 │ );
- │
- > 34 │ return evolu.getQueryRows(query);
- │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- 35 │ }
- 36 │
-
- i For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
-
- i See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
-
-
-packages/react/src/useQuerySubscription.ts:37:10 lint/correctness/useHookAtTopLevel ━━━━━━━━━━
-
- × This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
-
- 35 │ }
- 36 │
- > 37 │ return useSyncExternalStore(
- │ ^^^^^^^^^^^^^^^^^^^^
- 38 │ useMemo(() => evolu.subscribeQuery(query), [evolu, query]),
- 39 │ useMemo(() => () => evolu.getQueryRows(query), [evolu, query]),
-
- i Hooks should not be called after an early return.
-
- 31 │ () => evolu.subscribeQuery(query)(lazyVoid),
- 32 │ [evolu, query],
- > 33 │ );
- │
- > 34 │ return evolu.getQueryRows(query);
- │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- 35 │ }
- 36 │
-
- i For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
-
- i See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
-
-
-packages/vue/src/useOwner.ts:13:17 lint/correctness/useHookAtTopLevel ━━━━━━━━━━━━━━━━━
-
- × This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
-
- 11 │ if (owner == null) return;
- 12 │
- > 13 │ const evolu = useEvolu();
- │ ^^^^^^^^
- 14 │
- 15 │ evolu.useOwner(owner);
-
- i Hooks should not be called after an early return.
-
- 9 │ */
- 10 │ export const useOwner = (owner: SyncOwner | null): void => {
- > 11 │ if (owner == null) return;
- │ ^^^^^^^
- 12 │
- 13 │ const evolu = useEvolu();
-
- i For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
-
- i See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
-
-
-packages/vue/src/useOwner.ts:15:3 lint/correctness/useHookAtTopLevel ━━━━━━━━━━━━━━━━━━
-
- × This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
-
- 13 │ const evolu = useEvolu();
- 14 │
- > 15 │ evolu.useOwner(owner);
- │ ^^^^^^^^^^^^^^
- 16 │ };
- 17 │
-
- i Hooks should not be called after an early return.
-
- 9 │ */
- 10 │ export const useOwner = (owner: SyncOwner | null): void => {
- > 11 │ if (owner == null) return;
- │ ^^^^^^^
- 12 │
- 13 │ const evolu = useEvolu();
-
- i For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
-
- i See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
-
-
-packages/vue/src/useQueries.ts:38:12 lint/correctness/useHookAtTopLevel ━━━━━━━━━━━━━━━
-
- × This hook is being called from a nested function, but all hooks must be called unconditionally from the top-level component.
-
- 36 │ const queryOptions = { once: index > queries.length - 1 };
- 37 │
- > 38 │ return useQuery(
- │ ^^^^^^^^
- 39 │ query,
- 40 │ promise ? { ...queryOptions, promise } : queryOptions,
-
- i For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
-
- i See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
-
-
-Checked 361 files in 1666ms. No fixes applied.
-Found 11 errors.
-check ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-
- × Some errors were emitted while running checks.
-
-
diff --git a/examples/react-expo/app/index.tsx b/examples/react-expo/app/index.tsx
index 37c5ae8ba..0813afd7f 100644
--- a/examples/react-expo/app/index.tsx
+++ b/examples/react-expo/app/index.tsx
@@ -1,17 +1,34 @@
+import Alert from "@blazejkustra/react-native-alert";
import * as Evolu from "@evolu/common";
-import { createEvoluContext } from "@evolu/react";
-import { createRun } from "@evolu/react-native";
-import { createEvoluDeps } from "@evolu/react-native/expo-sqlite";
-import { Suspense, use } from "react";
-import { Text, View } from "react-native";
+import { createUseEvolu, EvoluProvider, useQuery } from "@evolu/react";
+import { EvoluIdenticon } from "@evolu/react-native";
+import {
+ evoluReactNativeDeps,
+ localAuth,
+} from "@evolu/react-native/expo-sqlite";
+import { type FC, Suspense, use, useEffect, useMemo, useState } from "react";
+import {
+ ActivityIndicator,
+ ScrollView,
+ StyleSheet,
+ Text,
+ TextInput,
+ TouchableOpacity,
+ View,
+} from "react-native";
+import { SafeAreaView } from "react-native-safe-area-context";
+
+// Namespace for the current app (scopes databases, passkeys, etc.)
+const service = "rn-expo";
// Primary keys are branded types, preventing accidental use of IDs across
// different tables (e.g., a TodoId can't be used where a UserId is expected).
const TodoId = Evolu.id("Todo");
+// biome-ignore lint/correctness/noUnusedVariables: Context
type TodoId = typeof TodoId.Type;
// Schema defines database structure with runtime validation.
-// Column types validate data on insert/update/upsert/sync.
+// Column types validate data on insert/update/upsert.
const Schema = {
todo: {
id: TodoId,
@@ -22,948 +39,820 @@ const Schema = {
},
};
-// Create Run with Evolu dependencies for React Native.
-const run = createRun(createEvoluDeps());
+export default function Index(): React.ReactNode {
+ const [authResult, setAuthResult] = useState(null);
+ const [ownerIds, setOwnerIds] = useState | null>(null);
+ const [evolu, setEvolu] = useState | null>(null);
+
+ useEffect(() => {
+ (async () => {
+ const authResult = await localAuth.getOwner({ service });
+ const ownerIds = await localAuth.getProfiles({ service });
+ const evolu = Evolu.createEvolu(evoluReactNativeDeps)(Schema, {
+ name: Evolu.SimpleName.orThrow(
+ `${service}-${authResult?.owner?.id ?? "guest"}`,
+ ),
+ encryptionKey: authResult?.owner?.encryptionKey,
+ externalAppOwner: authResult?.owner,
+ // ...(process.env.NODE_ENV === "development" && {
+ // transports: [{ type: "WebSocket", url: "ws://localhost:4000" }],
+ // }),
+ });
+
+ setEvolu(evolu as Evolu.Evolu);
+ setOwnerIds(ownerIds);
+ setAuthResult(authResult);
-// Create Evolu App.
-const app = run(
- Evolu.createEvolu(Schema, {
- name: Evolu.SimpleName.orThrow("rn-expo"),
- }),
-);
+ /**
+ * Subscribe to unexpected Evolu errors (database, network, sync issues).
+ * These should not happen in normal operation, so always log them for
+ * debugging. Show users a friendly error message instead of technical
+ * details.
+ */
+ return evolu.subscribeError(() => {
+ const error = evolu.getError();
+ if (!error) return;
+ Alert.alert("🚨 Evolu error occurred! Check the console.");
+ // eslint-disable-next-line no-console
+ console.error(error);
+ });
+ })().catch((error) => {
+ console.error(error);
+ });
+ }, []);
-const [App, AppProvider] = createEvoluContext(app);
+ if (evolu == null) {
+ return (
+
+
+
+ );
+ }
-export default function Index() {
return (
-
-
-
-
-
+
+
+
);
}
-const Test = () => {
- const app = use(App);
+const EvoluDemo = ({
+ evolu,
+ ownerIds,
+ authResult,
+}: {
+ evolu: Evolu.Evolu;
+ ownerIds: Array | null;
+ authResult: Evolu.AuthResult | null;
+}): React.ReactNode => {
+ const useEvolu = createUseEvolu(evolu);
+
+ // Create a query builder (once per schema).
+ const createQuery = Evolu.createQueryBuilder(Schema);
+
+ // Evolu uses Kysely for type-safe SQL (https://kysely.dev/).
+ const todosQuery = createQuery((db) =>
+ db
+ // Type-safe SQL: try autocomplete for table and column names.
+ .selectFrom("todo")
+ .select(["id", "title", "isCompleted"])
+ // Soft delete: filter out deleted rows.
+ .where("isDeleted", "is not", Evolu.sqliteTrue)
+ // Like with GraphQL, all columns except id are nullable in queries
+ // (even if defined without nullOr in the schema) to allow schema
+ // evolution without migrations. Filter nulls with where + $narrowType.
+ .where("title", "is not", null)
+ .$narrowType<{ title: Evolu.kysely.NotNull }>()
+ // Columns createdAt, updatedAt, isDeleted are auto-added to all tables.
+ .orderBy("createdAt"),
+ );
+
+ // Extract the row type from the query for type-safe component props.
+ type TodosRow = typeof todosQuery.Row;
+
+ const Todos: FC = () => {
+ // useQuery returns live data - component re-renders when data changes.
+ const todos = useQuery(todosQuery);
+ const { insert } = useEvolu();
+ const [newTodoTitle, setNewTodoTitle] = useState("");
+
+ const handleAddTodo = () => {
+ const result = insert(
+ "todo",
+ { title: newTodoTitle.trim() },
+ {
+ onComplete: () => {
+ setNewTodoTitle("");
+ },
+ },
+ );
+
+ if (!result.ok) {
+ Alert.alert("Error", formatTypeError(result.error));
+ }
+ };
+
+ return (
+ 0 ? 6 : 24 },
+ ]}
+ >
+ 0 ? "flex" : "none" },
+ ]}
+ >
+ {todos.map((todo) => (
+
+ ))}
+
+
+
+
+
+
+
+ );
+ };
+
+ const TodoItem: FC<{
+ row: TodosRow;
+ }> = ({ row: { id, title, isCompleted } }) => {
+ const { update } = useEvolu();
+
+ const handleToggleCompletedPress = () => {
+ update("todo", {
+ id,
+ isCompleted: Evolu.booleanToSqliteBoolean(!isCompleted),
+ });
+ };
+
+ const handleRenamePress = () => {
+ Alert.prompt(
+ "Edit Todo",
+ "Enter new title:",
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Save",
+ onPress: (newTitle?: string) => {
+ if (newTitle?.trim()) {
+ const result = update("todo", { id, title: newTitle.trim() });
+ if (!result.ok) {
+ Alert.alert("Error", formatTypeError(result.error));
+ }
+ }
+ },
+ },
+ ],
+ "plain-text",
+ title,
+ );
+ };
+
+ const handleDeletePress = () => {
+ update("todo", {
+ id,
+ // Soft delete with isDeleted flag (CRDT-friendly, preserves sync history).
+ isDeleted: Evolu.sqliteTrue,
+ });
+ };
+
+ return (
+
+
+
+
+ ✓
+
+
+
+ {title}
+
+
+
+
+
+ ✏️
+
+
+ 🗑️
+
+
+
+ );
+ };
+
+ const OwnerActions: FC = () => {
+ const evolu = useEvolu();
+ const appOwner = use(evolu.appOwner);
+ const [showMnemonic, setShowMnemonic] = useState(false);
+
+ // Restore owner from mnemonic to sync data across devices.
+ const handleRestoreAppOwnerPress = () => {
+ Alert.prompt(
+ "Restore Account",
+ "Enter your mnemonic to restore your data:",
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Restore",
+ onPress: (mnemonic?: string) => {
+ if (mnemonic == null) return;
+
+ const result = Evolu.Mnemonic.from(mnemonic.trim());
+ if (!result.ok) {
+ Alert.alert("Error", formatTypeError(result.error));
+ return;
+ }
+
+ void evolu.restoreAppOwner(result.value);
+ },
+ },
+ ],
+ "plain-text",
+ );
+ };
+
+ const handleResetAppOwnerPress = () => {
+ Alert.alert(
+ "Reset All Data",
+ "Are you sure? This will delete all your local data.",
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Reset",
+ style: "destructive",
+ onPress: () => {
+ void evolu.resetAppOwner();
+ },
+ },
+ ],
+ );
+ };
+
+ return (
+
+ Account
+ {appOwner && (
+
+
+
+ )}
+
+ Todos are stored in local SQLite. When you sync across devices, your
+ data is end-to-end encrypted using your mnemonic.
+
+
+
+ {
+ setShowMnemonic(!showMnemonic);
+ }}
+ style={styles.fullWidthButton}
+ />
+
+ {showMnemonic && appOwner?.mnemonic && (
+
+
+ Your Mnemonic (keep this safe!)
+
+
+
+ )}
+
+
+
+
+
+
+
+ );
+ };
+
+ const AuthActions: FC = () => {
+ const appOwner = use(evolu.appOwner);
+ // biome-ignore lint/correctness/useExhaustiveDependencies: Found ownerIds in outer scope
+ const otherOwnerIds = useMemo(
+ () => ownerIds?.filter(({ ownerId }) => ownerId !== appOwner?.id) ?? [],
+ [appOwner?.id, ownerIds],
+ );
+
+ // Create a new owner and register it to a passkey.
+ const handleRegisterPress = async () => {
+ Alert.prompt(
+ "Register Passkey",
+ "Enter your username:",
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Register",
+ onPress: async (username?: string) => {
+ if (username == null) return;
+
+ // Determine if this is a guest login or a new owner.
+ const isGuest = !authResult?.owner;
+
+ // Register the guest owner or create a new one if this is already registered.
+ const mnemonic = isGuest ? appOwner?.mnemonic : undefined;
+ const result = await localAuth.register(username, {
+ service,
+ mnemonic,
+ });
+ if (result) {
+ // If this is a guest owner, we should clear the database and reload.
+ // The owner is transferred to a new database on next login.
+ if (isGuest) {
+ evolu.resetAppOwner({ reload: true });
+ // Otherwise, just reload the app (in RN, we can't reload like web)
+ } else {
+ evolu.reloadApp();
+ }
+ } else {
+ Alert.alert("Error", "Failed to register profile");
+ }
+ },
+ },
+ ],
+ "plain-text",
+ );
+ };
+
+ // Login with a specific owner id using the registered passkey.
+ const handleLoginPress = async (ownerId: Evolu.OwnerId) => {
+ const result = await localAuth.login(ownerId, { service });
+ if (result) {
+ evolu.reloadApp();
+ } else {
+ Alert.alert("Error", "Failed to login");
+ }
+ };
+
+ // Clear all data including passkeys and metadata.
+ const handleClearAllPress = async () => {
+ Alert.alert(
+ "Clear All Data",
+ "Are you sure you want to clear all data? This will remove all passkeys and cannot be undone.",
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Clear",
+ style: "destructive",
+ onPress: async () => {
+ await localAuth.clearAll({ service });
+ void evolu.resetAppOwner({ reload: true });
+ },
+ },
+ ],
+ );
+ };
+
+ return (
+
+ Passkeys
+
+ Register a new passkey or choose a previously registered one.
+
+
+
+
+
+ {otherOwnerIds.length > 0 && (
+
+ {otherOwnerIds.map(({ ownerId, username }) => (
+
+ ))}
+
+ )}
+
+ );
+ };
+
+ const OwnerProfile: FC<{
+ ownerId: Evolu.OwnerId;
+ username: string;
+ handleLoginPress?: (ownerId: Evolu.OwnerId) => void;
+ }> = ({ ownerId, username, handleLoginPress }) => {
+ return (
+
+
+
+
+ {username}
+
+ {ownerId as string}
+
+
+
+ {handleLoginPress && (
+ handleLoginPress(ownerId)}
+ style={styles.loginButton}
+ />
+ )}
+
+ );
+ };
+
+ const CustomButton: FC<{
+ title: string;
+ style?: any;
+ onPress: () => void;
+ variant?: "primary" | "secondary";
+ }> = ({ title, style, onPress, variant = "secondary" }) => {
+ const buttonStyle = [
+ styles.button,
+ variant === "primary" ? styles.buttonPrimary : styles.buttonSecondary,
+ style,
+ ];
+
+ const textStyle = [
+ styles.buttonText,
+ variant === "primary"
+ ? styles.buttonTextPrimary
+ : styles.buttonTextSecondary,
+ ];
+
+ return (
+
+ {title}
+
+ );
+ };
return (
-
-
- {run.deps.random.next()} - {app.appOwner.id}
-
-
+
+
+
+
+ Minimal Todo App (Evolu + Expo)
+
+
+ {/*
+ Suspense delivers great UX (no loading flickers) and DX (no loading
+ states to manage). Highly recommended with Evolu.
+ */}
+
+
+
+
+
+
+
+
+
);
};
-// import * as Evolu from "@evolu/common";
-// import { createEvoluContext } from "@evolu/react";
-// import { createRun } from "@evolu/react-native";
-// import { evoluReactNativeDeps } from "@evolu/react-native/expo-sqlite";
-// import { Suspense, use } from "react";
-// import { Text, View } from "react-native";
-//
-// // Primary keys are branded types, preventing accidental use of IDs across
-// // different tables (e.g., a TodoId can't be used where a UserId is expected).
-// const TodoId = Evolu.id("Todo");
-// type TodoId = typeof TodoId.Type;
-//
-// // Schema defines database structure with runtime validation.
-// // Column types validate data on insert/update/upsert/sync.
-// const Schema = {
-// todo: {
-// id: TodoId,
-// // Branded type ensuring titles are non-empty and ≤100 chars.
-// title: Evolu.NonEmptyString100,
-// // SQLite doesn't support the boolean type; it uses 0 and 1 instead.
-// isCompleted: Evolu.nullOr(Evolu.SqliteBoolean),
-// },
-// };
-//
-// // Create Run with Evolu dependencies for React Native.
-// const run = createRun(evoluReactNativeDeps);
-//
-// // Create Evolu App.
-// const app = run(
-// Evolu.createEvolu(Schema, {
-// name: Evolu.SimpleName.orThrow("rn-expo"),
-// }),
-// );
-//
-// const [App, AppProvider] = createEvoluContext(app);
-//
-// export default function Index() {
-// return (
-//
-//
-//
-//
-//
-// );
-// }
-//
-// const Test = () => {
-// const app = use(App);
-//
-// return (
-//
-//
-// {run.deps.random.next()} - {app.appOwner.id}
-//
-//
-// );
-// };
-//
-// // import Alert from "@blazejkustra/react-native-alert";
-// // import * as Evolu from "@evolu/common";
-// // import { createUseEvolu, EvoluProvider, useQuery } from "@evolu/react";
-// // import { EvoluIdenticon } from "@evolu/react-native";
-// // import {
-// // evoluReactNativeDeps,
-// // localAuth,
-// // } from "@evolu/react-native/expo-sqlite";
-// // import { FC, Suspense, use, useEffect, useMemo, useState } from "react";
-// // import {
-// // ActivityIndicator,
-// // ScrollView,
-// // StyleSheet,
-// // Text,
-// // TextInput,
-// // TouchableOpacity,
-// // View,
-// // } from "react-native";
-// // import { SafeAreaView } from "react-native-safe-area-context";
-// //
-// // // Namespace for the current app (scopes databases, passkeys, etc.)
-// // const service = "rn-expo";
-// //
-// // // Primary keys are branded types, preventing accidental use of IDs across
-// // // different tables (e.g., a TodoId can't be used where a UserId is expected).
-// // const TodoId = Evolu.id("Todo");
-// // type TodoId = typeof TodoId.Type;
-// //
-// // // Schema defines database structure with runtime validation.
-// // // Column types validate data on insert/update/upsert.
-// // const Schema = {
-// // todo: {
-// // id: TodoId,
-// // // Branded type ensuring titles are non-empty and ≤100 chars.
-// // title: Evolu.NonEmptyString100,
-// // // SQLite doesn't support the boolean type; it uses 0 and 1 instead.
-// // isCompleted: Evolu.nullOr(Evolu.SqliteBoolean),
-// // },
-// // };
-// //
-// // export default function Index(): React.ReactNode {
-// // const [authResult, setAuthResult] = useState(null);
-// // const [ownerIds, setOwnerIds] = useState | null>(null);
-// // const [evolu, setEvolu] = useState | null>(null);
-// //
-// // useEffect(() => {
-// // (async () => {
-// // const authResult = await localAuth.getOwner({ service });
-// // const ownerIds = await localAuth.getProfiles({ service });
-// // const evolu = Evolu.createEvolu(evoluReactNativeDeps)(Schema, {
-// // name: Evolu.SimpleName.orThrow(
-// // `${service}-${authResult?.owner?.id ?? "guest"}`,
-// // ),
-// // encryptionKey: authResult?.owner?.encryptionKey,
-// // externalAppOwner: authResult?.owner,
-// // // ...(process.env.NODE_ENV === "development" && {
-// // // transports: [{ type: "WebSocket", url: "ws://localhost:4000" }],
-// // // }),
-// // });
-// //
-// // setEvolu(evolu as Evolu.Evolu);
-// // setOwnerIds(ownerIds);
-// // setAuthResult(authResult);
-// //
-// // /**
-// // * Subscribe to unexpected Evolu errors (database, network, sync issues).
-// // * These should not happen in normal operation, so always log them for
-// // * debugging. Show users a friendly error message instead of technical
-// // * details.
-// // */
-// // return evolu.subscribeError(() => {
-// // const error = evolu.getError();
-// // if (!error) return;
-// // Alert.alert("🚨 Evolu error occurred! Check the console.");
-// // // eslint-disable-next-line no-console
-// // console.error(error);
-// // });
-// // })().catch((error) => {
-// // console.error(error);
-// // });
-// // }, []);
-// //
-// // if (evolu == null) {
-// // return (
-// //
-// //
-// //
-// // );
-// // }
-// //
-// // return (
-// //
-// //
-// //
-// // );
-// // }
-// //
-// // const EvoluDemo = ({
-// // evolu,
-// // ownerIds,
-// // authResult,
-// // }: {
-// // evolu: Evolu.Evolu;
-// // ownerIds: Array | null;
-// // authResult: Evolu.AuthResult | null;
-// // }): React.ReactNode => {
-// // const useEvolu = createUseEvolu(evolu);
-// //
-// // // Evolu uses Kysely for type-safe SQL (https://kysely.dev/).
-// // const todosQuery = evolu.createQuery((db) =>
-// // db
-// // // Type-safe SQL: try autocomplete for table and column names.
-// // .selectFrom("todo")
-// // .select(["id", "title", "isCompleted"])
-// // // Soft delete: filter out deleted rows.
-// // .where("isDeleted", "is not", Evolu.sqliteTrue)
-// // // Like with GraphQL, all columns except id are nullable in queries
-// // // (even if defined without nullOr in the schema) to allow schema
-// // // evolution without migrations. Filter nulls with where + $narrowType.
-// // .where("title", "is not", null)
-// // .$narrowType<{ title: Evolu.kysely.NotNull }>()
-// // // Columns createdAt, updatedAt, isDeleted are auto-added to all tables.
-// // .orderBy("createdAt"),
-// // );
-// //
-// // // Extract the row type from the query for type-safe component props.
-// // type TodosRow = typeof todosQuery.Row;
-// //
-// // const Todos: FC = () => {
-// // // useQuery returns live data - component re-renders when data changes.
-// // const todos = useQuery(todosQuery);
-// // const { insert } = useEvolu();
-// // const [newTodoTitle, setNewTodoTitle] = useState("");
-// //
-// // const handleAddTodo = () => {
-// // const result = insert(
-// // "todo",
-// // { title: newTodoTitle.trim() },
-// // {
-// // onComplete: () => {
-// // setNewTodoTitle("");
-// // },
-// // },
-// // );
-// //
-// // if (!result.ok) {
-// // Alert.alert("Error", formatTypeError(result.error));
-// // }
-// // };
-// //
-// // return (
-// // 0 ? 6 : 24 },
-// // ]}
-// // >
-// // 0 ? "flex" : "none" },
-// // ]}
-// // >
-// // {todos.map((todo) => (
-// //
-// // ))}
-// //
-// //
-// //
-// //
-// //
-// //
-// //
-// // );
-// // };
-// //
-// // const TodoItem: FC<{
-// // row: TodosRow;
-// // }> = ({ row: { id, title, isCompleted } }) => {
-// // const { update } = useEvolu();
-// //
-// // const handleToggleCompletedPress = () => {
-// // update("todo", {
-// // id,
-// // isCompleted: Evolu.booleanToSqliteBoolean(!isCompleted),
-// // });
-// // };
-// //
-// // const handleRenamePress = () => {
-// // Alert.prompt(
-// // "Edit Todo",
-// // "Enter new title:",
-// // [
-// // { text: "Cancel", style: "cancel" },
-// // {
-// // text: "Save",
-// // onPress: (newTitle?: string) => {
-// // if (newTitle != null && newTitle.trim()) {
-// // const result = update("todo", { id, title: newTitle.trim() });
-// // if (!result.ok) {
-// // Alert.alert("Error", formatTypeError(result.error));
-// // }
-// // }
-// // },
-// // },
-// // ],
-// // "plain-text",
-// // title,
-// // );
-// // };
-// //
-// // const handleDeletePress = () => {
-// // update("todo", {
-// // id,
-// // // Soft delete with isDeleted flag (CRDT-friendly, preserves sync history).
-// // isDeleted: Evolu.sqliteTrue,
-// // });
-// // };
-// //
-// // return (
-// //
-// //
-// //
-// //
-// // ✓
-// //
-// //
-// //
-// // {title}
-// //
-// //
-// //
-// //
-// //
-// // ✏️
-// //
-// //
-// // 🗑️
-// //
-// //
-// //
-// // );
-// // };
-// //
-// // const OwnerActions: FC = () => {
-// // const evolu = useEvolu();
-// // const appOwner = use(evolu.appOwner);
-// // const [showMnemonic, setShowMnemonic] = useState(false);
-// //
-// // // Restore owner from mnemonic to sync data across devices.
-// // const handleRestoreAppOwnerPress = () => {
-// // Alert.prompt(
-// // "Restore Account",
-// // "Enter your mnemonic to restore your data:",
-// // [
-// // { text: "Cancel", style: "cancel" },
-// // {
-// // text: "Restore",
-// // onPress: (mnemonic?: string) => {
-// // if (mnemonic == null) return;
-// //
-// // const result = Evolu.Mnemonic.from(mnemonic.trim());
-// // if (!result.ok) {
-// // Alert.alert("Error", formatTypeError(result.error));
-// // return;
-// // }
-// //
-// // void evolu.restoreAppOwner(result.value);
-// // },
-// // },
-// // ],
-// // "plain-text",
-// // );
-// // };
-// //
-// // const handleResetAppOwnerPress = () => {
-// // Alert.alert(
-// // "Reset All Data",
-// // "Are you sure? This will delete all your local data.",
-// // [
-// // { text: "Cancel", style: "cancel" },
-// // {
-// // text: "Reset",
-// // style: "destructive",
-// // onPress: () => {
-// // void evolu.resetAppOwner();
-// // },
-// // },
-// // ],
-// // );
-// // };
-// //
-// // return (
-// //
-// // Account
-// // {appOwner && (
-// //
-// //
-// //
-// // )}
-// //
-// // Todos are stored in local SQLite. When you sync across devices, your
-// // data is end-to-end encrypted using your mnemonic.
-// //
-// //
-// //
-// // {
-// // setShowMnemonic(!showMnemonic);
-// // }}
-// // style={styles.fullWidthButton}
-// // />
-// //
-// // {showMnemonic && appOwner?.mnemonic && (
-// //
-// //
-// // Your Mnemonic (keep this safe!)
-// //
-// //
-// //
-// // )}
-// //
-// //
-// //
-// //
-// //
-// //
-// //
-// // );
-// // };
-// //
-// // const AuthActions: FC = () => {
-// // const appOwner = use(evolu.appOwner);
-// // const otherOwnerIds = useMemo(
-// // () => ownerIds?.filter(({ ownerId }) => ownerId !== appOwner?.id) ?? [],
-// // [appOwner?.id, ownerIds],
-// // );
-// //
-// // // Create a new owner and register it to a passkey.
-// // const handleRegisterPress = async () => {
-// // Alert.prompt(
-// // "Register Passkey",
-// // "Enter your username:",
-// // [
-// // { text: "Cancel", style: "cancel" },
-// // {
-// // text: "Register",
-// // onPress: async (username?: string) => {
-// // if (username == null) return;
-// //
-// // // Determine if this is a guest login or a new owner.
-// // const isGuest = !Boolean(authResult?.owner);
-// //
-// // // Register the guest owner or create a new one if this is already registered.
-// // const mnemonic = isGuest ? appOwner?.mnemonic : undefined;
-// // const result = await localAuth.register(username, {
-// // service,
-// // mnemonic,
-// // });
-// // if (result) {
-// // // If this is a guest owner, we should clear the database and reload.
-// // // The owner is transferred to a new database on next login.
-// // if (isGuest) {
-// // evolu.resetAppOwner({ reload: true });
-// // // Otherwise, just reload the app (in RN, we can't reload like web)
-// // } else {
-// // evolu.reloadApp();
-// // }
-// // } else {
-// // Alert.alert("Error", "Failed to register profile");
-// // }
-// // },
-// // },
-// // ],
-// // "plain-text",
-// // );
-// // };
-// //
-// // // Login with a specific owner id using the registered passkey.
-// // const handleLoginPress = async (ownerId: Evolu.OwnerId) => {
-// // const result = await localAuth.login(ownerId, { service });
-// // if (result) {
-// // evolu.reloadApp();
-// // } else {
-// // Alert.alert("Error", "Failed to login");
-// // }
-// // };
-// //
-// // // Clear all data including passkeys and metadata.
-// // const handleClearAllPress = async () => {
-// // Alert.alert(
-// // "Clear All Data",
-// // "Are you sure you want to clear all data? This will remove all passkeys and cannot be undone.",
-// // [
-// // { text: "Cancel", style: "cancel" },
-// // {
-// // text: "Clear",
-// // style: "destructive",
-// // onPress: async () => {
-// // await localAuth.clearAll({ service });
-// // void evolu.resetAppOwner({ reload: true });
-// // },
-// // },
-// // ],
-// // );
-// // };
-// //
-// // return (
-// //
-// // Passkeys
-// //
-// // Register a new passkey or choose a previously registered one.
-// //
-// //
-// //
-// //
-// //
-// // {otherOwnerIds.length > 0 && (
-// //
-// // {otherOwnerIds.map(({ ownerId, username }) => (
-// //
-// // ))}
-// //
-// // )}
-// //
-// // );
-// // };
-// //
-// // const OwnerProfile: FC<{
-// // ownerId: Evolu.OwnerId;
-// // username: string;
-// // handleLoginPress?: (ownerId: Evolu.OwnerId) => void;
-// // }> = ({ ownerId, username, handleLoginPress }) => {
-// // return (
-// //
-// //
-// //
-// //
-// // {username}
-// //
-// // {ownerId as string}
-// //
-// //
-// //
-// // {handleLoginPress && (
-// // handleLoginPress(ownerId)}
-// // style={styles.loginButton}
-// // />
-// // )}
-// //
-// // );
-// // };
-// //
-// // const CustomButton: FC<{
-// // title: string;
-// // style?: any;
-// // onPress: () => void;
-// // variant?: "primary" | "secondary";
-// // }> = ({ title, style, onPress, variant = "secondary" }) => {
-// // const buttonStyle = [
-// // styles.button,
-// // variant === "primary" ? styles.buttonPrimary : styles.buttonSecondary,
-// // style,
-// // ];
-// //
-// // const textStyle = [
-// // styles.buttonText,
-// // variant === "primary"
-// // ? styles.buttonTextPrimary
-// // : styles.buttonTextSecondary,
-// // ];
-// //
-// // return (
-// //
-// // {title}
-// //
-// // );
-// // };
-// //
-// // return (
-// //
-// //
-// //
-// //
-// // Minimal Todo App (Evolu + Expo)
-// //
-// //
-// // {/*
-// // Suspense delivers great UX (no loading flickers) and DX (no loading
-// // states to manage). Highly recommended with Evolu.
-// // */}
-// //
-// //
-// //
-// //
-// //
-// //
-// //
-// //
-// //
-// // );
-// // };
-// //
-// // const styles = StyleSheet.create({
-// // container: {
-// // flex: 1,
-// // backgroundColor: "#f9fafb",
-// // },
-// // scrollView: {
-// // flex: 1,
-// // },
-// // contentContainer: {
-// // flexGrow: 1,
-// // paddingHorizontal: 32,
-// // paddingVertical: 32,
-// // },
-// // maxWidthContainer: {
-// // maxWidth: 400,
-// // alignSelf: "center",
-// // width: "100%",
-// // },
-// // header: {
-// // marginBottom: 8,
-// // paddingBottom: 16,
-// // alignItems: "center",
-// // },
-// // title: {
-// // fontSize: 20,
-// // fontWeight: "600",
-// // color: "#111827",
-// // textAlign: "center",
-// // },
-// // todosContainer: {
-// // backgroundColor: "#ffffff",
-// // borderRadius: 8,
-// // paddingHorizontal: 24,
-// // paddingVertical: 24,
-// // paddingBottom: 24,
-// // shadowColor: "#000",
-// // shadowOffset: { width: 0, height: 1 },
-// // shadowOpacity: 0.05,
-// // shadowRadius: 3,
-// // elevation: 1,
-// // borderWidth: 1,
-// // borderColor: "#e5e7eb",
-// // },
-// // todosList: {
-// // marginBottom: 24,
-// // },
-// // todoItem: {
-// // flexDirection: "row",
-// // alignItems: "center",
-// // paddingVertical: 8,
-// // paddingHorizontal: -8,
-// // marginHorizontal: -8,
-// // },
-// // todoCheckbox: {
-// // flexDirection: "row",
-// // alignItems: "center",
-// // flex: 1,
-// // },
-// // checkbox: {
-// // width: 16,
-// // height: 16,
-// // borderRadius: 2,
-// // borderWidth: 1,
-// // borderColor: "#d1d5db",
-// // backgroundColor: "#ffffff",
-// // marginRight: 12,
-// // alignItems: "center",
-// // justifyContent: "center",
-// // },
-// // checkboxChecked: {
-// // backgroundColor: "#3b82f6",
-// // borderColor: "#3b82f6",
-// // },
-// // checkmark: {
-// // color: "#ffffff",
-// // fontSize: 10,
-// // fontWeight: "bold",
-// // },
-// // todoTitle: {
-// // fontSize: 14,
-// // color: "#111827",
-// // flex: 1,
-// // },
-// // todoTitleCompleted: {
-// // color: "#6b7280",
-// // textDecorationLine: "line-through",
-// // },
-// // todoActions: {
-// // flexDirection: "row",
-// // gap: 4,
-// // },
-// // actionButton: {
-// // padding: 4,
-// // },
-// // editIcon: {
-// // fontSize: 16,
-// // },
-// // deleteIcon: {
-// // fontSize: 16,
-// // },
-// // addTodoContainer: {
-// // flexDirection: "row",
-// // gap: 8,
-// // marginHorizontal: -8,
-// // },
-// // textInput: {
-// // flex: 1,
-// // borderRadius: 6,
-// // backgroundColor: "#ffffff",
-// // paddingHorizontal: 12,
-// // paddingVertical: 6,
-// // fontSize: 16,
-// // color: "#111827",
-// // borderWidth: 1,
-// // borderColor: "#d1d5db",
-// // },
-// // button: {
-// // paddingHorizontal: 12,
-// // paddingVertical: 8,
-// // borderRadius: 8,
-// // alignItems: "center",
-// // justifyContent: "center",
-// // },
-// // buttonPrimary: {
-// // backgroundColor: "#3b82f6",
-// // },
-// // buttonSecondary: {
-// // backgroundColor: "#f3f4f6",
-// // },
-// // buttonText: {
-// // fontSize: 14,
-// // fontWeight: "500",
-// // },
-// // buttonTextPrimary: {
-// // color: "#ffffff",
-// // },
-// // buttonTextSecondary: {
-// // color: "#374151",
-// // },
-// // ownerActionsContainer: {
-// // marginTop: 32,
-// // backgroundColor: "#ffffff",
-// // borderRadius: 8,
-// // padding: 24,
-// // paddingTop: 18,
-// // shadowColor: "#000",
-// // shadowOffset: { width: 0, height: 1 },
-// // shadowOpacity: 0.05,
-// // shadowRadius: 3,
-// // elevation: 1,
-// // borderWidth: 1,
-// // borderColor: "#e5e7eb",
-// // },
-// // sectionTitle: {
-// // fontSize: 18,
-// // fontWeight: "500",
-// // color: "#111827",
-// // marginBottom: 16,
-// // },
-// // sectionDescription: {
-// // fontSize: 14,
-// // color: "#6b7280",
-// // marginBottom: 16,
-// // lineHeight: 20,
-// // },
-// // ownerActionsButtons: {
-// // gap: 12,
-// // },
-// // fullWidthButton: {
-// // width: "100%",
-// // },
-// // mnemonicContainer: {
-// // backgroundColor: "#f9fafb",
-// // padding: 12,
-// // borderRadius: 6,
-// // },
-// // mnemonicLabel: {
-// // fontSize: 12,
-// // fontWeight: "500",
-// // color: "#374151",
-// // marginBottom: 8,
-// // },
-// // mnemonicTextArea: {
-// // backgroundColor: "#ffffff",
-// // borderBottomWidth: 1,
-// // borderBottomColor: "#d1d5db",
-// // paddingHorizontal: 8,
-// // paddingVertical: 4,
-// // fontFamily: "monospace",
-// // fontSize: 12,
-// // color: "#111827",
-// // },
-// // actionButtonsRow: {
-// // flexDirection: "row",
-// // gap: 8,
-// // },
-// // flexButton: {
-// // flex: 1,
-// // },
-// // authActionsContainer: {
-// // marginTop: 32,
-// // backgroundColor: "#ffffff",
-// // borderRadius: 8,
-// // padding: 24,
-// // paddingTop: 18,
-// // shadowColor: "#000",
-// // shadowOffset: { width: 0, height: 1 },
-// // shadowOpacity: 0.05,
-// // shadowRadius: 3,
-// // elevation: 1,
-// // borderWidth: 1,
-// // borderColor: "#e5e7eb",
-// // },
-// // ownerProfileContainer: {
-// // marginBottom: 16,
-// // },
-// // ownerProfileRow: {
-// // flexDirection: "row",
-// // justifyContent: "space-between",
-// // alignItems: "center",
-// // paddingVertical: 12,
-// // paddingHorizontal: 12,
-// // backgroundColor: "#f9fafb",
-// // borderRadius: 6,
-// // marginBottom: 8,
-// // },
-// // ownerInfo: {
-// // flexDirection: "row",
-// // alignItems: "center",
-// // flex: 1,
-// // gap: 8,
-// // marginRight: 12,
-// // },
-// // ownerDetails: {
-// // flex: 1,
-// // },
-// // ownerUsername: {
-// // fontSize: 14,
-// // fontWeight: "500",
-// // color: "#111827",
-// // marginBottom: 2,
-// // },
-// // ownerIdText: {
-// // fontSize: 10,
-// // color: "#6b7280",
-// // fontStyle: "italic",
-// // },
-// // loginButton: {
-// // paddingHorizontal: 16,
-// // },
-// // otherOwnersContainer: {
-// // marginTop: 16,
-// // },
-// // });
-// //
-// // /**
-// // * Formats Evolu Type errors into user-friendly messages.
-// // *
-// // * Evolu Type typed errors ensure every error type used in schema must have a
-// // * formatter. TypeScript enforces this at compile-time, preventing unhandled
-// // * validation errors from reaching users.
-// // *
-// // * The `createFormatTypeError` function handles both built-in and custom errors,
-// // * and lets us override default formatting for specific errors.
-// // */
-// // const formatTypeError = Evolu.createFormatTypeError<
-// // Evolu.MinLengthError | Evolu.MaxLengthError
-// // >((error): string => {
-// // switch (error.type) {
-// // case "MinLength":
-// // return `Text must be at least ${error.min} character${error.min === 1 ? "" : "s"} long`;
-// // case "MaxLength":
-// // return `Text is too long (maximum ${error.max} characters)`;
-// // }
-// // });
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: "#f9fafb",
+ },
+ scrollView: {
+ flex: 1,
+ },
+ contentContainer: {
+ flexGrow: 1,
+ paddingHorizontal: 32,
+ paddingVertical: 32,
+ },
+ maxWidthContainer: {
+ maxWidth: 400,
+ alignSelf: "center",
+ width: "100%",
+ },
+ header: {
+ marginBottom: 8,
+ paddingBottom: 16,
+ alignItems: "center",
+ },
+ title: {
+ fontSize: 20,
+ fontWeight: "600",
+ color: "#111827",
+ textAlign: "center",
+ },
+ todosContainer: {
+ backgroundColor: "#ffffff",
+ borderRadius: 8,
+ paddingHorizontal: 24,
+ paddingVertical: 24,
+ paddingBottom: 24,
+ shadowColor: "#000",
+ shadowOffset: { width: 0, height: 1 },
+ shadowOpacity: 0.05,
+ shadowRadius: 3,
+ elevation: 1,
+ borderWidth: 1,
+ borderColor: "#e5e7eb",
+ },
+ todosList: {
+ marginBottom: 24,
+ },
+ todoItem: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingVertical: 8,
+ paddingHorizontal: -8,
+ marginHorizontal: -8,
+ },
+ todoCheckbox: {
+ flexDirection: "row",
+ alignItems: "center",
+ flex: 1,
+ },
+ checkbox: {
+ width: 16,
+ height: 16,
+ borderRadius: 2,
+ borderWidth: 1,
+ borderColor: "#d1d5db",
+ backgroundColor: "#ffffff",
+ marginRight: 12,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ checkboxChecked: {
+ backgroundColor: "#3b82f6",
+ borderColor: "#3b82f6",
+ },
+ checkmark: {
+ color: "#ffffff",
+ fontSize: 10,
+ fontWeight: "bold",
+ },
+ todoTitle: {
+ fontSize: 14,
+ color: "#111827",
+ flex: 1,
+ },
+ todoTitleCompleted: {
+ color: "#6b7280",
+ textDecorationLine: "line-through",
+ },
+ todoActions: {
+ flexDirection: "row",
+ gap: 4,
+ },
+ actionButton: {
+ padding: 4,
+ },
+ editIcon: {
+ fontSize: 16,
+ },
+ deleteIcon: {
+ fontSize: 16,
+ },
+ addTodoContainer: {
+ flexDirection: "row",
+ gap: 8,
+ marginHorizontal: -8,
+ },
+ textInput: {
+ flex: 1,
+ borderRadius: 6,
+ backgroundColor: "#ffffff",
+ paddingHorizontal: 12,
+ paddingVertical: 6,
+ fontSize: 16,
+ color: "#111827",
+ borderWidth: 1,
+ borderColor: "#d1d5db",
+ },
+ button: {
+ paddingHorizontal: 12,
+ paddingVertical: 8,
+ borderRadius: 8,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ buttonPrimary: {
+ backgroundColor: "#3b82f6",
+ },
+ buttonSecondary: {
+ backgroundColor: "#f3f4f6",
+ },
+ buttonText: {
+ fontSize: 14,
+ fontWeight: "500",
+ },
+ buttonTextPrimary: {
+ color: "#ffffff",
+ },
+ buttonTextSecondary: {
+ color: "#374151",
+ },
+ ownerActionsContainer: {
+ marginTop: 32,
+ backgroundColor: "#ffffff",
+ borderRadius: 8,
+ padding: 24,
+ paddingTop: 18,
+ shadowColor: "#000",
+ shadowOffset: { width: 0, height: 1 },
+ shadowOpacity: 0.05,
+ shadowRadius: 3,
+ elevation: 1,
+ borderWidth: 1,
+ borderColor: "#e5e7eb",
+ },
+ sectionTitle: {
+ fontSize: 18,
+ fontWeight: "500",
+ color: "#111827",
+ marginBottom: 16,
+ },
+ sectionDescription: {
+ fontSize: 14,
+ color: "#6b7280",
+ marginBottom: 16,
+ lineHeight: 20,
+ },
+ ownerActionsButtons: {
+ gap: 12,
+ },
+ fullWidthButton: {
+ width: "100%",
+ },
+ mnemonicContainer: {
+ backgroundColor: "#f9fafb",
+ padding: 12,
+ borderRadius: 6,
+ },
+ mnemonicLabel: {
+ fontSize: 12,
+ fontWeight: "500",
+ color: "#374151",
+ marginBottom: 8,
+ },
+ mnemonicTextArea: {
+ backgroundColor: "#ffffff",
+ borderBottomWidth: 1,
+ borderBottomColor: "#d1d5db",
+ paddingHorizontal: 8,
+ paddingVertical: 4,
+ fontFamily: "monospace",
+ fontSize: 12,
+ color: "#111827",
+ },
+ actionButtonsRow: {
+ flexDirection: "row",
+ gap: 8,
+ },
+ flexButton: {
+ flex: 1,
+ },
+ authActionsContainer: {
+ marginTop: 32,
+ backgroundColor: "#ffffff",
+ borderRadius: 8,
+ padding: 24,
+ paddingTop: 18,
+ shadowColor: "#000",
+ shadowOffset: { width: 0, height: 1 },
+ shadowOpacity: 0.05,
+ shadowRadius: 3,
+ elevation: 1,
+ borderWidth: 1,
+ borderColor: "#e5e7eb",
+ },
+ ownerProfileContainer: {
+ marginBottom: 16,
+ },
+ ownerProfileRow: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ paddingVertical: 12,
+ paddingHorizontal: 12,
+ backgroundColor: "#f9fafb",
+ borderRadius: 6,
+ marginBottom: 8,
+ },
+ ownerInfo: {
+ flexDirection: "row",
+ alignItems: "center",
+ flex: 1,
+ gap: 8,
+ marginRight: 12,
+ },
+ ownerDetails: {
+ flex: 1,
+ },
+ ownerUsername: {
+ fontSize: 14,
+ fontWeight: "500",
+ color: "#111827",
+ marginBottom: 2,
+ },
+ ownerIdText: {
+ fontSize: 10,
+ color: "#6b7280",
+ fontStyle: "italic",
+ },
+ loginButton: {
+ paddingHorizontal: 16,
+ },
+ otherOwnersContainer: {
+ marginTop: 16,
+ },
+});
+
+/**
+ * Formats Evolu Type errors into user-friendly messages.
+ *
+ * Evolu Type typed errors ensure every error type used in schema must have a
+ * formatter. TypeScript enforces this at compile-time, preventing unhandled
+ * validation errors from reaching users.
+ *
+ * The `createFormatTypeError` function handles both built-in and custom errors,
+ * and lets us override default formatting for specific errors.
+ */
+const formatTypeError = Evolu.createFormatTypeError<
+ Evolu.MinLengthError | Evolu.MaxLengthError
+>((error): string => {
+ switch (error.type) {
+ case "MinLength":
+ return `Text must be at least ${error.min} character${error.min === 1 ? "" : "s"} long`;
+ case "MaxLength":
+ return `Text is too long (maximum ${error.max} characters)`;
+ }
+});
diff --git a/examples/react-expo/package.json b/examples/react-expo/package.json
index d705be8eb..2065706b6 100644
--- a/examples/react-expo/package.json
+++ b/examples/react-expo/package.json
@@ -11,7 +11,7 @@
"ios": "expo run:ios",
"ios:go": "expo start --ios",
"lint": "expo lint",
- "_start": "expo start"
+ "start": "expo start"
},
"jest": {
"preset": "jest-expo"
diff --git a/packages/common/src/local-first/Evolu.ts b/packages/common/src/local-first/Evolu.ts
index 55fe7c950..895f66045 100644
--- a/packages/common/src/local-first/Evolu.ts
+++ b/packages/common/src/local-first/Evolu.ts
@@ -11,12 +11,7 @@ import type { Task } from "../Task.js";
import type { SimpleName } from "../Type.js";
import type { EvoluError } from "./Error.js";
import type { AppOwner, OwnerTransport } from "./Owner.js";
-import {
- createAppOwner,
- createOwnerSecret,
- createOwnerWebSocketTransport,
- OwnerId,
-} from "./Owner.js";
+import { createAppOwner, createOwnerSecret } from "./Owner.js";
import type {
Queries,
QueriesToQueryRowsPromises,
diff --git a/packages/common/test/local-first/Evolu.test.ts b/packages/common/test/local-first/Evolu.test.ts
index d561d6bc9..6e1a0f18f 100644
--- a/packages/common/test/local-first/Evolu.test.ts
+++ b/packages/common/test/local-first/Evolu.test.ts
@@ -8,7 +8,6 @@ import { testSimpleName } from "../_deps.js";
import { testAppOwner } from "./_fixtures.js";
const TodoId = id("Todo");
-type TodoId = typeof TodoId.Type;
const Schema = {
todo: {
diff --git a/packages/react-native/src/exports/expo-op-sqlite.ts b/packages/react-native/src/exports/expo-op-sqlite.ts
index d2cecb91b..480bb7a4b 100644
--- a/packages/react-native/src/exports/expo-op-sqlite.ts
+++ b/packages/react-native/src/exports/expo-op-sqlite.ts
@@ -1,15 +1,15 @@
-// /**
-// * Public entry point for Expo with OP-SQLite. Exported as
-// * "@evolu/react-native/expo-op-sqlite" in package.json.
-// *
-// * Use this with Expo projects that use `@op-engineering/op-sqlite` for better
-// * performance.
-// */
-//
-// import { createExpoDeps } from "../createExpoDeps.js";
-// import { createOpSqliteDriver } from "../sqlite-drivers/createOpSqliteDriver.js";
-//
-// // eslint-disable-next-line evolu/require-pure-annotation
-// export const { evoluReactNativeDeps, localAuth } = createExpoDeps({
-// createSqliteDriver: createOpSqliteDriver,
-// });
+/**
+ * Public entry point for Expo with OP-SQLite. Exported as
+ * "@evolu/react-native/expo-op-sqlite" in package.json.
+ *
+ * Use this with Expo projects that use `@op-engineering/op-sqlite` for better
+ * performance.
+ */
+
+import { createExpoDeps } from "../createExpoDeps.js";
+import { createOpSqliteDriver } from "../sqlite-drivers/createOpSqliteDriver.js";
+
+// eslint-disable-next-line evolu/require-pure-annotation
+export const { evoluReactNativeDeps, localAuth } = createExpoDeps({
+ createSqliteDriver: createOpSqliteDriver,
+});
diff --git a/packages/react/src/EvoluProvider.tsx b/packages/react/src/EvoluProvider.tsx
new file mode 100644
index 000000000..17002ebad
--- /dev/null
+++ b/packages/react/src/EvoluProvider.tsx
@@ -0,0 +1,15 @@
+"use client";
+
+import type { Evolu } from "@evolu/common/local-first";
+import type { ReactNode } from "react";
+import { EvoluContext } from "./local-first/EvoluContext.js";
+
+export const EvoluProvider = ({
+ children,
+ value,
+}: {
+ readonly children?: ReactNode | undefined;
+ readonly value: Evolu;
+}): React.ReactElement => (
+ {children}
+);
diff --git a/packages/react/src/Task.tsx b/packages/react/src/Task.tsx
index 286a22cad..6e693888f 100644
--- a/packages/react/src/Task.tsx
+++ b/packages/react/src/Task.tsx
@@ -1,9 +1,9 @@
"use client";
-import { type Run, testCreateRun } from "@evolu/common";
+import type { Run } from "@evolu/common";
import { createContext, type ReactNode } from "react";
-const RunContext = /*#__PURE__*/ createContext(testCreateRun());
+const RunContext = /*#__PURE__*/ createContext(null);
/**
* Creates typed React Context and Provider for {@link Run}.
@@ -30,6 +30,6 @@ export const createRunContext = (
} => ({
Run: RunContext as React.Context>,
RunProvider: ({ children }) => (
- {children}
+ {children}
),
});
diff --git a/packages/react/src/createUseEvolu.ts b/packages/react/src/createUseEvolu.ts
new file mode 100644
index 000000000..76897ffe2
--- /dev/null
+++ b/packages/react/src/createUseEvolu.ts
@@ -0,0 +1,17 @@
+import type { Evolu, EvoluSchema } from "@evolu/common";
+import { useEvolu } from "./useEvolu.js";
+
+/**
+ * Creates a typed React Hook returning an instance of {@link Evolu}.
+ *
+ * ### Example
+ *
+ * ```ts
+ * const useEvolu = createUseEvolu(evolu);
+ * const { insert, update } = useEvolu();
+ * ```
+ */
+export const createUseEvolu = (
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ evolu: Evolu,
+): (() => Evolu) => useEvolu as () => Evolu;
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index c91fa40b1..4b49acdbf 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -1,3 +1,5 @@
+export * from "./createUseEvolu.js";
+export * from "./EvoluProvider.js";
export * from "./local-first/EvoluContext.js";
export * from "./local-first/useIsSsr.js";
export * from "./local-first/useOwner.js";
diff --git a/packages/react/src/local-first/EvoluContext.tsx b/packages/react/src/local-first/EvoluContext.tsx
index 60e181073..69dd0f03c 100644
--- a/packages/react/src/local-first/EvoluContext.tsx
+++ b/packages/react/src/local-first/EvoluContext.tsx
@@ -39,12 +39,18 @@ export const createEvoluContext = (
): readonly [
React.Context>,
React.FC<{ readonly children?: ReactNode }>,
-] => [
- EvoluContext as React.Context>,
- ({ children }) => {
- const result = use(fiber);
- assert(result.ok, "createEvolu failed");
+] => {
+ const Context = /*#__PURE__*/ createContext>(null as never);
- return {children} ;
- },
-];
+ return [
+ Context,
+ ({ children }) => {
+ const result = use(fiber);
+ assert(result.ok, "createEvolu failed");
+
+ return (
+ {children}
+ );
+ },
+ ];
+};
diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts
index 59d2b9b0c..f763257ce 100644
--- a/packages/vue/src/index.ts
+++ b/packages/vue/src/index.ts
@@ -2,6 +2,7 @@ export * from "./createUseEvolu.js";
export * from "./EvoluProvider.js";
export * from "./provideEvolu.js";
export * from "./useEvolu.js";
+export * from "./useEvoluError.js";
export * from "./useOwner.js";
export * from "./useQueries.js";
export * from "./useQuery.js";
diff --git a/packages/vue/src/useEvoluError.ts b/packages/vue/src/useEvoluError.ts
new file mode 100644
index 000000000..0b77b11c5
--- /dev/null
+++ b/packages/vue/src/useEvoluError.ts
@@ -0,0 +1,17 @@
+import type { EvoluError } from "@evolu/common";
+import { onScopeDispose, type Ref, ref } from "vue";
+import { useEvolu } from "./useEvolu.js";
+
+/** Subscribe to {@link EvoluError} changes. */
+export const useEvoluError = (): Ref => {
+ const evolu = useEvolu();
+ const error = ref(evolu.getError());
+
+ const unsubscribe = evolu.subscribeError(() => {
+ error.value = evolu.getError();
+ });
+
+ onScopeDispose(unsubscribe);
+
+ return error;
+};