Quickstart
Let's learn how to use Evolu in a few steps.
Define Data
First, we define a database schema: tables, columns, and their types.
Evolu uses
Schema (opens in a new tab) for
data modeling. Instead of plain JavaScript types like String or Number, we
recommend branded types (opens in a new tab). With
branded types, we can define and enforce domain rules like
NonEmptyString1000
or PositiveInt
.
import * as S from "@effect/schema/Schema";
import {
NonEmptyString1000,
SqliteBoolean,
createEvolu,
id,
table,
database,
} from "@evolu/react";
const TodoId = id("Todo");
type TodoId = typeof TodoId.Type;
const TodoTable = table({
id: TodoId,
title: NonEmptyString1000,
isCompleted: SqliteBoolean,
});
type TodoTable = typeof TodoTable.Type;
const Database = database({
todo: TodoTable,
});
type Database = typeof Database.Type;
const evolu = createEvolu(Database);
TypeScript compiler ensures that the title
can't be an arbitrary string.
It has to be parsed with the NonEmptyString1000
schema. Isn't that beautiful?
Parse Data
import * as S from "@effect/schema/Schema";
import { NonEmptyString1000 } from "@evolu/react";
S.decode(NonEmptyString1000)(title);
Learn more about Schema (opens in a new tab). It's like Zod (opens in a new tab), but faster and with better design.
Mutate Data
While Evolu provides the full SQL for queries, the mutation API is tailored for local-first apps to ensure changes can be merged without conflicts and to mitigate the possibility that a developer accidentally makes unwanted changes—for example, an update of all rows in a table. That would generate a lot of CRDT messages that would have to be propagated to all other devices. It's not bad per se; it's just something that shouldn't be necessary with proper database schema design.
// Without React
const { id } = evolu.create("todo", { title, isCompleted: false });
evolu.update("todo", { id, isCompleted: true }, () => {
// done
});
// With React
const { create, update } = useEvolu<Database>();
Note there is no error handling because there is no reason why a mutation
should fail. Types ensure correctness, and the local SQLite database is always
available. For rare cases where Evolu can fail, use global
evolu.subscribeError
or useEvoluError
React Hook.
To delete a row, set isDeleted
to true and filter "deleted" rows in queries.
CRDT without an authoritative server doesn't delete data; it just marks them as deleted. This is not a bug; it's a feature that allows data to be merged without conflicts (overridden data can be restored).
Query Data
Evolu uses type-safe TypeScript SQL query builder
Kysely (opens in a new tab), so autocompletion works
out-of-the-box. Let's start with a simple Query
.
const allTodos = evolu.createQuery((db) => db.selectFrom("todo").selectAll());
Once we have a query, we can load or subscribe to it.
const promise = evolu.loadQuery(allTodos);
const unsubscribe = evolu.subscribeQuery(allTodos)(callback);
Evolu provides React Hooks useQuery
and useQueries
with the full React
Suspense support. For example, we can load a query in the root and use the
returned promise elsewhere.
Protect Data
Privacy is essential for Evolu, so all data are encrypted with an encryption key derived from a safely generated cryptographically strong password called mnemonic (opens in a new tab).
// evolu.subscribeOwner
const owner = evolu.getOwner();
if (owner) owner.mnemonic;
Delete Data
Leave no traces on a device.
if (confirm("Are you sure? It will delete all your local data."))
evolu.resetOwner();
Restore Data
Synced Evolu data can be restored with mnemonic on any device.
evolu.restoreOwner(mnemonic);
And that's all we need to know to work with Evolu. The minimal API is the key to good DX.