Skip to content

Commit

Permalink
Suspense (#153)
Browse files Browse the repository at this point in the history
  • Loading branch information
steida authored May 22, 2023
1 parent 1bcee72 commit c7f5182
Show file tree
Hide file tree
Showing 48 changed files with 1,668 additions and 1,406 deletions.
11 changes: 11 additions & 0 deletions .changeset/fluffy-crews-sparkle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"evolu": major
---

React Suspense

[It's about time](https://twitter.com/acdlite/status/1654171173582692353). React Suspense is an excellent React feature that massively improves both UX and DX. It's a breaking change because I decided to remove the `isLoading` and `isLoaded` states entirely. It's not necessary anymore. Use React Suspense.

Implementing and testing React Suspense also led to internal optimizations for faster and more reliable syncing and better unit tests.

This release also includes SQLite 3.42.0. There is no breaking change in data persistence.
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ Evolu is designed for privacy, ease of use, and no vendor lock-in.
- E2E encrypted sync and backup with CRDT (merging changes without conflicts)
- Free Evolu server for testing (paid production-ready soon, or you can run your own)
- Typed database schema with branded types (`NonEmptyString1000`, `PositiveInt`, etc.)
- Reactive queries
- Reactive queries with React Suspense support
- Real-time experience via revalidation on focus and network recovery
- Schema evolving via `filterMap` ad-hoc migration
- No signup/login, no email collection, only Bitcoin-like mnemonic (12 words)
- React Suspense (soon)

## Local-first apps

Expand Down Expand Up @@ -75,7 +74,7 @@ export const {
useOwner,
useOwnerActions,
useEvoluError,
} = Evolu.createHooks(Database);
} = Evolu.create(Database);
```

### Validate Data
Expand Down Expand Up @@ -188,8 +187,7 @@ It should be. The CRDT message format is stable.

### What is the SQLite database size limit?

Evolu uses OPFS in Chrome and Firefox and LocalStorage in Safari.
The size limit of OPFS is 256 MB (LocalStorage 5 MB).
(Storage_quotas_and_eviction_criteria)[https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria]

### How can I check the current database filesize?

Expand Down
6 changes: 3 additions & 3 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
},
"dependencies": {
"evolu-server": "workspace:1.0.0"
"evolu-server": "workspace:*"
},
"devDependencies": {
"@evolu/tsconfig": "workspace:0.0.2",
"@types/node": "^18.16.3",
"@evolu/tsconfig": "workspace:*",
"@types/node": "^20.2.3",
"ts-node": "^10.9.1",
"typescript": "^5.0.4"
},
Expand Down
136 changes: 80 additions & 56 deletions apps/web/components/NextJsExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ import { pipe } from "@effect/data/Function";
import * as Schema from "@effect/schema/Schema";
import { formatErrors } from "@effect/schema/TreeFormatter";
import * as Evolu from "evolu";
import { ChangeEvent, FC, memo, useEffect, useState } from "react";
import {
ChangeEvent,
FC,
Suspense,
memo,
startTransition,
useEffect,
useState,
} from "react";

const TodoId = Evolu.id("Todo");
type TodoId = Schema.To<typeof TodoId>;
Expand Down Expand Up @@ -38,7 +46,7 @@ const Database = Schema.struct({
});

const { useQuery, useMutation, useEvoluError, useOwner, useOwnerActions } =
Evolu.createHooks(Database, {
Evolu.create(Database, {
reloadUrl: "/examples/nextjs",
...(process.env.NODE_ENV === "development" && {
syncUrl: "http://localhost:4000",
Expand Down Expand Up @@ -66,33 +74,39 @@ const Button: FC<{
}> = ({ title, onClick }) => {
return (
<button
className="m-1 rounded-md border border-current px-1 text-sm"
className="m-1 rounded-md border border-current px-1 text-sm active:opacity-80"
onClick={onClick}
>
{title}
</button>
);
};

const TodoCategorySelect: FC<{
selected: TodoCategoryId | null;
onSelect: (value: TodoCategoryId | null) => void;
}> = ({ selected, onSelect }) => {
const { rows } = useQuery(
type TodoCategoriesList = ReadonlyArray<{
id: TodoCategoryId;
name: NonEmptyString50;
}>;

const useTodoCategoriesList = (): TodoCategoriesList =>
useQuery(
(db) =>
db
.selectFrom("todoCategory")
.select(["id", "name"])
.where("isDeleted", "is not", Evolu.cast(true))
.orderBy("createdAt"),
// (row) => row
// Filter out rows with nullable names.
({ name, ...rest }) => name && { name, ...rest }
);
).rows;

const TodoCategorySelect: FC<{
selected: TodoCategoryId | null;
onSelect: (value: TodoCategoryId | null) => void;
todoCategoriesList: TodoCategoriesList;
}> = ({ selected, onSelect, todoCategoriesList }) => {
const nothingSelected = "";
const value =
selected && rows.find((row) => row.id === selected)
selected && todoCategoriesList.find((row) => row.id === selected)
? selected
: nothingSelected;

Expand All @@ -106,7 +120,7 @@ const TodoCategorySelect: FC<{
}}
>
<option value={nothingSelected}>-- no category --</option>
{rows.map(({ id, name }) => (
{todoCategoriesList.map(({ id, name }) => (
<option key={id} value={id}>
{name}
</option>
Expand All @@ -117,7 +131,11 @@ const TodoCategorySelect: FC<{

const TodoItem = memo<{
row: Pick<TodoTable, "id" | "title" | "isCompleted" | "categoryId">;
}>(function TodoItem({ row: { id, title, isCompleted, categoryId } }) {
todoCategoriesList: TodoCategoriesList;
}>(function TodoItem({
row: { id, title, isCompleted, categoryId },
todoCategoriesList,
}) {
const { update } = useMutation();

return (
Expand Down Expand Up @@ -149,6 +167,7 @@ const TodoItem = memo<{
}}
/>
<TodoCategorySelect
todoCategoriesList={todoCategoriesList}
selected={categoryId}
onSelect={(categoryId): void => {
update("todo", { id, categoryId });
Expand All @@ -158,7 +177,8 @@ const TodoItem = memo<{
);
});

const TodoList: FC = () => {
const Todos: FC = () => {
const { create } = useMutation();
const { rows } = useQuery(
(db) =>
db
Expand All @@ -170,35 +190,38 @@ const TodoList: FC = () => {
({ title, isCompleted, ...rest }) =>
title && isCompleted != null && { title, isCompleted, ...rest }
);
const todoCategoriesList = useTodoCategoriesList();

return (
<>
<h2 className="mt-6 text-xl font-semibold">Todos</h2>
<ul className="py-2">
{rows.map((row) => (
<TodoItem key={row.id} row={row} />
<TodoItem
key={row.id}
row={row}
todoCategoriesList={todoCategoriesList}
/>
))}
</ul>
<Button
title="Add Todo"
onClick={(): void => {
prompt(
Evolu.NonEmptyString1000,
"What needs to be done?",
(title) => {
create("todo", { title, isCompleted: false });
}
);
}}
/>
</>
);
};

const AddTodo: FC = () => {
const { create } = useMutation();

return (
<Button
title="Add Todo"
onClick={(): void => {
prompt(Evolu.NonEmptyString1000, "What needs to be done?", (title) => {
create("todo", { title, isCompleted: false });
});
}}
/>
);
};

const TodoCategoryList: FC = () => {
const TodoCategories: FC = () => {
const { create, update } = useMutation();
const { rows } = useQuery(
(db) =>
db
Expand All @@ -210,8 +233,6 @@ const TodoCategoryList: FC = () => {
({ name, ...rest }) => name && { name, ...rest }
);

const { update } = useMutation();

return (
<>
<h2 className="mt-6 text-xl font-semibold">Categories</h2>
Expand All @@ -236,25 +257,18 @@ const TodoCategoryList: FC = () => {
</li>
))}
</ul>
<Button
title="Add Category"
onClick={(): void => {
prompt(NonEmptyString50, "Category Name", (name) => {
create("todoCategory", { name });
});
}}
/>
</>
);
};

const AddTodoCategory: FC = () => {
const { create } = useMutation();

return (
<Button
title="Add Category"
onClick={(): void => {
prompt(NonEmptyString50, "Category Name", (name) => {
create("todoCategory", { name });
});
}}
/>
);
};

const OwnerActions: FC = () => {
const [isShown, setIsShown] = useState(false);
const owner = useOwner();
Expand Down Expand Up @@ -320,15 +334,25 @@ const NotificationBar: FC = () => {
);
};

export const NextJsExample = memo(function NextJsExample() {
export const NextJsExample: FC = () => {
const [todosShown, setTodosShown] = useState(true);

return (
<>
<Suspense>
<NotificationBar />
<TodoList />
<AddTodo />
<TodoCategoryList />
<AddTodoCategory />
<nav className="my-4">
<Button
title="Simulate suspense-enabled router transition"
onClick={(): void => {
// https://react.dev/reference/react/useTransition#building-a-suspense-enabled-router
startTransition(() => {
setTodosShown(!todosShown);
});
}}
/>
</nav>
{todosShown ? <Todos /> : <TodoCategories />}
<OwnerActions />
</>
</Suspense>
);
});
};
24 changes: 12 additions & 12 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,26 @@
},
"dependencies": {
"@effect/data": "^0.12.2",
"@effect/schema": "~0.17.4",
"@effect/schema": "~0.19.3",
"clsx": "^1.2.1",
"evolu": "workspace:5.0.0",
"next": "^13.4.0",
"nextra": "^2.5.0",
"nextra-theme-docs": "^2.5.0",
"evolu": "workspace:*",
"next": "^13.4.3",
"nextra": "^2.6.0",
"nextra-theme-docs": "^2.6.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@evolu/tsconfig": "workspace:0.0.2",
"@types/node": "^18.16.3",
"@types/react": "^18.2.5",
"@types/react-dom": "^18.2.3",
"@evolu/tsconfig": "workspace:*",
"@types/node": "^20.2.3",
"@types/react": "^18.2.6",
"@types/react-dom": "^18.2.4",
"autoprefixer": "^10.4.14",
"eslint": "^8.39.0",
"eslint-config-evolu": "workspace:0.0.2",
"eslint": "^8.41.0",
"eslint-config-evolu": "workspace:*",
"postcss": "^8.4.23",
"prettier": "^2.8.8",
"prettier-plugin-tailwindcss": "^0.2.8",
"prettier-plugin-tailwindcss": "^0.3.0",
"tailwindcss": "^3.3.2",
"typescript": "^5.0.4"
}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/pages/docs/api/_meta.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"create-hooks": "createHooks",
"create": "create",
"use-mutation": "useMutation",
"use-query": "useQuery",
"use-evolu-error": "useEvoluError",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# createHooks
# create

`createHooks` defines the database schema and returns React Hooks.
`create` defines the database schema and returns React Hooks.
Evolu uses [Schema](https://github.com/effect-ts/schema) for domain modeling.

## Example
Expand Down Expand Up @@ -29,7 +29,7 @@ export const {
useEvoluError,
useOwner,
useOwnerActions,
} = Evolu.createHooks(Database);
} = Evolu.create(Database);
```

import { Callout } from "nextra-theme-docs";
Expand Down
16 changes: 9 additions & 7 deletions apps/web/pages/docs/api/use-query.mdx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# useQuery

`useQuery` React Hook performs a database query and returns rows that
are automatically updated when data changes.
`useQuery` React Hook performs a database query and returns `rows` and
`firstRow` props that are automatically updated when data changes. It
takes two callbacks, a [Kysely](https://github.com/koskimas/kysely)
type-safe SQL query builder, and a `filterMap` helper for rows filtering
and ad-hoc migrations.

It takes two callbacks, a [Kysely](https://github.com/koskimas/kysely) type-safe SQL query builder,
and a `filterMap` helper.
import { Callout } from "nextra-theme-docs";

`useQuery` also returns `isLoaded` and `isLoading` props that indicate
loading progress. `isLoaded` becomes true when rows are loaded for the
first time. `isLoading` becomes true whenever rows are loading.
<Callout type="info" emoji="ℹ️">
Note that `useQuery` uses React Suspense.
</Callout>

## Examples

Expand Down
Loading

1 comment on commit c7f5182

@vercel
Copy link

@vercel vercel bot commented on c7f5182 May 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

evolu – ./

evolu-git-main-evolu.vercel.app
evolu-evolu.vercel.app
www.evolu.dev
evolu.dev
evolu.vercel.app

Please sign in to comment.