@livestore/livestore 0.4.0-dev.21 → 0.4.0-dev.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/README.md +0 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/QueryCache.js +1 -1
  4. package/dist/QueryCache.js.map +1 -1
  5. package/dist/SqliteDbWrapper.d.ts +5 -5
  6. package/dist/SqliteDbWrapper.d.ts.map +1 -1
  7. package/dist/SqliteDbWrapper.js +8 -8
  8. package/dist/SqliteDbWrapper.js.map +1 -1
  9. package/dist/SqliteDbWrapper.test.js +2 -2
  10. package/dist/SqliteDbWrapper.test.js.map +1 -1
  11. package/dist/effect/LiveStore.d.ts +130 -2
  12. package/dist/effect/LiveStore.d.ts.map +1 -1
  13. package/dist/effect/LiveStore.js +185 -6
  14. package/dist/effect/LiveStore.js.map +1 -1
  15. package/dist/effect/LiveStore.test.d.ts +2 -0
  16. package/dist/effect/LiveStore.test.d.ts.map +1 -0
  17. package/dist/effect/LiveStore.test.js +42 -0
  18. package/dist/effect/LiveStore.test.js.map +1 -0
  19. package/dist/effect/mod.d.ts +1 -1
  20. package/dist/effect/mod.d.ts.map +1 -1
  21. package/dist/effect/mod.js +3 -1
  22. package/dist/effect/mod.js.map +1 -1
  23. package/dist/live-queries/base-class.d.ts +3 -3
  24. package/dist/live-queries/base-class.d.ts.map +1 -1
  25. package/dist/live-queries/base-class.js +2 -2
  26. package/dist/live-queries/base-class.js.map +1 -1
  27. package/dist/live-queries/client-document-get-query.d.ts +1 -1
  28. package/dist/live-queries/client-document-get-query.d.ts.map +1 -1
  29. package/dist/live-queries/client-document-get-query.js +1 -1
  30. package/dist/live-queries/client-document-get-query.js.map +1 -1
  31. package/dist/live-queries/computed.d.ts.map +1 -1
  32. package/dist/live-queries/computed.js +2 -2
  33. package/dist/live-queries/computed.js.map +1 -1
  34. package/dist/live-queries/db-query.js +14 -14
  35. package/dist/live-queries/db-query.js.map +1 -1
  36. package/dist/live-queries/db-query.test.js +2 -2
  37. package/dist/live-queries/db-query.test.js.map +1 -1
  38. package/dist/live-queries/signal.test.js +2 -2
  39. package/dist/live-queries/signal.test.js.map +1 -1
  40. package/dist/mod.d.ts +2 -1
  41. package/dist/mod.d.ts.map +1 -1
  42. package/dist/mod.js +1 -0
  43. package/dist/mod.js.map +1 -1
  44. package/dist/reactive.d.ts +9 -9
  45. package/dist/reactive.d.ts.map +1 -1
  46. package/dist/reactive.js +9 -26
  47. package/dist/reactive.js.map +1 -1
  48. package/dist/reactive.test.js +2 -2
  49. package/dist/reactive.test.js.map +1 -1
  50. package/dist/store/StoreRegistry.d.ts +215 -0
  51. package/dist/store/StoreRegistry.d.ts.map +1 -0
  52. package/dist/store/StoreRegistry.js +267 -0
  53. package/dist/store/StoreRegistry.js.map +1 -0
  54. package/dist/store/StoreRegistry.test.d.ts +2 -0
  55. package/dist/store/StoreRegistry.test.d.ts.map +1 -0
  56. package/dist/store/StoreRegistry.test.js +381 -0
  57. package/dist/store/StoreRegistry.test.js.map +1 -0
  58. package/dist/store/create-store.d.ts +56 -6
  59. package/dist/store/create-store.d.ts.map +1 -1
  60. package/dist/store/create-store.js +32 -7
  61. package/dist/store/create-store.js.map +1 -1
  62. package/dist/store/devtools.d.ts +1 -1
  63. package/dist/store/devtools.d.ts.map +1 -1
  64. package/dist/store/devtools.js +16 -3
  65. package/dist/store/devtools.js.map +1 -1
  66. package/dist/store/store-eventstream.test.js +2 -2
  67. package/dist/store/store-eventstream.test.js.map +1 -1
  68. package/dist/store/store-types.d.ts +59 -9
  69. package/dist/store/store-types.d.ts.map +1 -1
  70. package/dist/store/store-types.js.map +1 -1
  71. package/dist/store/store-types.test.js +1 -1
  72. package/dist/store/store-types.test.js.map +1 -1
  73. package/dist/store/store.d.ts +102 -6
  74. package/dist/store/store.d.ts.map +1 -1
  75. package/dist/store/store.js +148 -47
  76. package/dist/store/store.js.map +1 -1
  77. package/dist/utils/dev.js.map +1 -1
  78. package/dist/utils/stack-info.js +2 -2
  79. package/dist/utils/stack-info.js.map +1 -1
  80. package/dist/utils/tests/fixture.d.ts +1 -1
  81. package/dist/utils/tests/fixture.d.ts.map +1 -1
  82. package/dist/utils/tests/fixture.js.map +1 -1
  83. package/dist/utils/tests/otel.d.ts.map +1 -1
  84. package/dist/utils/tests/otel.js +5 -5
  85. package/dist/utils/tests/otel.js.map +1 -1
  86. package/package.json +59 -18
  87. package/src/QueryCache.ts +1 -1
  88. package/src/SqliteDbWrapper.test.ts +4 -2
  89. package/src/SqliteDbWrapper.ts +12 -11
  90. package/src/ambient.d.ts +0 -7
  91. package/src/effect/LiveStore.test.ts +61 -0
  92. package/src/effect/LiveStore.ts +381 -8
  93. package/src/effect/mod.ts +13 -1
  94. package/src/live-queries/__snapshots__/db-query.test.ts.snap +336 -231
  95. package/src/live-queries/base-class.ts +7 -6
  96. package/src/live-queries/client-document-get-query.ts +4 -2
  97. package/src/live-queries/computed.ts +3 -2
  98. package/src/live-queries/db-query.test.ts +3 -2
  99. package/src/live-queries/db-query.ts +15 -15
  100. package/src/live-queries/signal.test.ts +3 -2
  101. package/src/mod.ts +2 -0
  102. package/src/reactive.test.ts +3 -2
  103. package/src/reactive.ts +22 -23
  104. package/src/store/StoreRegistry.test.ts +540 -0
  105. package/src/store/StoreRegistry.ts +418 -0
  106. package/src/store/create-store.ts +76 -15
  107. package/src/store/devtools.ts +20 -6
  108. package/src/store/store-eventstream.test.ts +4 -2
  109. package/src/store/store-types.test.ts +3 -1
  110. package/src/store/store-types.ts +64 -13
  111. package/src/store/store.ts +197 -60
  112. package/src/utils/dev.ts +2 -2
  113. package/src/utils/stack-info.ts +2 -2
  114. package/src/utils/tests/fixture.ts +2 -1
  115. package/src/utils/tests/otel.ts +8 -7
  116. package/docs/api/index.md +0 -3
  117. package/docs/building-with-livestore/complex-ui-state/index.md +0 -5
  118. package/docs/building-with-livestore/crud/index.md +0 -5
  119. package/docs/building-with-livestore/data-modeling/index.md +0 -1
  120. package/docs/building-with-livestore/debugging/index.md +0 -17
  121. package/docs/building-with-livestore/devtools/index.md +0 -79
  122. package/docs/building-with-livestore/events/index.md +0 -355
  123. package/docs/building-with-livestore/examples/ai-agent/index.md +0 -5
  124. package/docs/building-with-livestore/examples/index.md +0 -30
  125. package/docs/building-with-livestore/examples/todo-workspaces/index.md +0 -891
  126. package/docs/building-with-livestore/examples/turnbased-game/index.md +0 -7
  127. package/docs/building-with-livestore/opentelemetry/index.md +0 -208
  128. package/docs/building-with-livestore/production-checklist/index.md +0 -5
  129. package/docs/building-with-livestore/reactivity-system/index.md +0 -202
  130. package/docs/building-with-livestore/rules-for-ai-agents/index.md +0 -9
  131. package/docs/building-with-livestore/state/materializers/index.md +0 -300
  132. package/docs/building-with-livestore/state/sql-queries/index.md +0 -72
  133. package/docs/building-with-livestore/state/sqlite/index.md +0 -45
  134. package/docs/building-with-livestore/state/sqlite-schema/index.md +0 -306
  135. package/docs/building-with-livestore/state/sqlite-schema-effect/index.md +0 -300
  136. package/docs/building-with-livestore/store/index.md +0 -281
  137. package/docs/building-with-livestore/syncing/index.md +0 -136
  138. package/docs/building-with-livestore/tools/cli/index.md +0 -177
  139. package/docs/building-with-livestore/tools/mcp/index.md +0 -187
  140. package/docs/examples/cloudflare-adapter/index.md +0 -44
  141. package/docs/examples/expo-adapter/index.md +0 -44
  142. package/docs/examples/index.md +0 -55
  143. package/docs/examples/node-adapter/index.md +0 -44
  144. package/docs/examples/web-adapter/index.md +0 -52
  145. package/docs/framework-integrations/custom-elements/index.md +0 -142
  146. package/docs/framework-integrations/react-integration/index.md +0 -918
  147. package/docs/framework-integrations/solid-integration/index.md +0 -293
  148. package/docs/framework-integrations/svelte-integration/index.md +0 -42
  149. package/docs/framework-integrations/vue-integration/index.md +0 -294
  150. package/docs/getting-started/expo/index.md +0 -736
  151. package/docs/getting-started/node/index.md +0 -115
  152. package/docs/getting-started/react-web/index.md +0 -573
  153. package/docs/getting-started/solid/index.md +0 -3
  154. package/docs/getting-started/vue/index.md +0 -471
  155. package/docs/index.md +0 -209
  156. package/docs/llms.txt +0 -147
  157. package/docs/misc/CODE_OF_CONDUCT/index.md +0 -133
  158. package/docs/misc/FAQ/index.md +0 -37
  159. package/docs/misc/community/index.md +0 -88
  160. package/docs/misc/credits/index.md +0 -14
  161. package/docs/misc/design-partners/index.md +0 -13
  162. package/docs/misc/package-management/index.md +0 -21
  163. package/docs/misc/performance/index.md +0 -25
  164. package/docs/misc/resources/index.md +0 -46
  165. package/docs/misc/state-of-the-project/index.md +0 -37
  166. package/docs/misc/troubleshooting/index.md +0 -82
  167. package/docs/overview/concepts/index.md +0 -78
  168. package/docs/overview/how-livestore-works/index.md +0 -56
  169. package/docs/overview/introduction/index.md +0 -5
  170. package/docs/overview/technology-comparison/index.md +0 -40
  171. package/docs/overview/when-livestore/index.md +0 -81
  172. package/docs/overview/why-livestore/index.md +0 -5
  173. package/docs/patterns/ai/index.md +0 -15
  174. package/docs/patterns/anonymous-user-transition/index.md +0 -10
  175. package/docs/patterns/app-evolution/index.md +0 -72
  176. package/docs/patterns/auth/index.md +0 -226
  177. package/docs/patterns/effect/index.md +0 -1495
  178. package/docs/patterns/encryption/index.md +0 -6
  179. package/docs/patterns/external-data/index.md +0 -5
  180. package/docs/patterns/file-management/index.md +0 -11
  181. package/docs/patterns/file-structure/index.md +0 -14
  182. package/docs/patterns/list-ordering/index.md +0 -369
  183. package/docs/patterns/offline/index.md +0 -32
  184. package/docs/patterns/orm/index.md +0 -18
  185. package/docs/patterns/presence/index.md +0 -11
  186. package/docs/patterns/rich-text-editing/index.md +0 -11
  187. package/docs/patterns/server-side-clients/index.md +0 -97
  188. package/docs/patterns/side-effects/index.md +0 -11
  189. package/docs/patterns/state-machines/index.md +0 -11
  190. package/docs/patterns/storybook/index.md +0 -192
  191. package/docs/patterns/undo-redo/index.md +0 -9
  192. package/docs/patterns/version-control/index.md +0 -8
  193. package/docs/platform-adapters/cloudflare-durable-object-adapter/index.md +0 -453
  194. package/docs/platform-adapters/electron-adapter/index.md +0 -15
  195. package/docs/platform-adapters/expo-adapter/index.md +0 -245
  196. package/docs/platform-adapters/node-adapter/index.md +0 -160
  197. package/docs/platform-adapters/tauri-adapter/index.md +0 -15
  198. package/docs/platform-adapters/web-adapter/index.md +0 -218
  199. package/docs/sustainable-open-source/contributing/docs/index.md +0 -94
  200. package/docs/sustainable-open-source/contributing/info/index.md +0 -63
  201. package/docs/sustainable-open-source/contributing/monorepo/index.md +0 -195
  202. package/docs/sustainable-open-source/sponsoring/index.md +0 -104
  203. package/docs/sync-providers/cloudflare/index.md +0 -773
  204. package/docs/sync-providers/custom/index.md +0 -65
  205. package/docs/sync-providers/electricsql/index.md +0 -159
  206. package/docs/sync-providers/s2/index.md +0 -230
  207. package/docs/tutorial/0-welcome/index.md +0 -48
  208. package/docs/tutorial/1-setup-starter-project/index.md +0 -105
  209. package/docs/tutorial/2-deploy-to-cloudflare/index.md +0 -195
  210. package/docs/tutorial/3-read-and-write-todos-via-livestore/index.md +0 -511
  211. package/docs/tutorial/4-sync-data-via-cloudflare/index.md +0 -210
  212. package/docs/tutorial/5-expand-business-logic/index.md +0 -174
  213. package/docs/tutorial/6-persist-ui-state/index.md +0 -453
  214. package/docs/tutorial/7-next-steps/index.md +0 -22
  215. package/docs/understanding-livestore/design-decisions/index.md +0 -33
  216. package/docs/understanding-livestore/event-sourcing/index.md +0 -40
@@ -1,453 +0,0 @@
1
- # 6. Persist UI state
2
-
3
- As you saw in the beginning of the tutorial, the state of an application is typically divided into two categories:
4
-
5
- - **App state**: State that represents the _application data_ a user needs to achieve their goals with your app. In your current case, that's the list of todos. App state _typically_ lives in the Cloud somewhere (in traditional apps, the Cloud is the source of truth for it; in local-first apps, the data is stored locally and backed up in the Cloud).
6
- - **UI state**: UI state that is only relevant for a particular browser session.
7
-
8
- Many websites have the problem of "losing UI state" on browser refreshes. This can be incredibly frustrating for users, especially when they've already invested a lot of time getting to a certain point in an app (e.g. filling out a form). Then, the site reloads for some reason and they have to start over!
9
-
10
- With LiveStore, this problem is easily solved: It allows you to persist _UI state_ (e.g. form inputs, active tabs, custom UI elements, and pretty much anything you'd otherwise manage via `React.useState`). This means users can always pick up exactly where they left off.
11
-
12
- ## Add another UI element
13
-
14
- First, update `App.tsx` to look as follows:
15
-
16
- ```diff title="src/App.tsx" lang="tsx"
17
-
18
- function App() {
19
-
20
- const { store } = useStore()
21
-
22
- const todos$ = queryDb(() => tables.todos.select())
23
- const todos = store.useQuery(todos$)
24
-
25
- const [input, setInput] = useState('')
26
- + const [filter, setFilter] = useState<'All' | 'Active' | 'Completed'>('All')
27
-
28
- const addTodo = () => {
29
- if (input.trim()) {
30
- store.commit(
31
- events.todoCreated({ id: Date.now(), text: input }),
32
- )
33
- setInput('')
34
- }
35
- }
36
-
37
- const deleteTodo = (id: number) => {
38
- store.commit(
39
- events.todoDeleted({ id }),
40
- )
41
- }
42
-
43
- const toggleTodo = (id: number, completed: boolean) => {
44
- store.commit(
45
- completed ? events.todoUncompleted({ id }) : events.todoCompleted({ id })
46
- )
47
- }
48
-
49
- const handleKeyDown = (e: React.KeyboardEvent) => {
50
- if (e.key === 'Enter') {
51
- addTodo()
52
- }
53
- }
54
-
55
- return (
56
- <div className="min-h-screen bg-gray-50 flex items-center justify-center p-6">
57
- <div className="w-full max-w-lg">
58
- <h1 className="text-5xl font-bold text-gray-800 text-center mb-12">
59
- Todo List
60
- </h1>
61
-
62
- <div className="flex gap-3 mb-8">
63
- <input
64
- type="text"
65
- value={input}
66
- onChange={(e) => setInput(e.target.value)}
67
- onKeyDown={handleKeyDown}
68
- placeholder="Enter a todo..."
69
- className="flex-1 px-4 py-2 text-sm border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
70
- />
71
- <button
72
- onClick={addTodo}
73
- className="px-6 py-2 text-sm font-medium text-white bg-blue-500 rounded hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
74
- >
75
- Add
76
- </button>
77
- </div>
78
-
79
- + <div className="flex gap-1 mb-4 border-b border-gray-200 justify-center">
80
- + {(['All', 'Active', 'Completed'] as const).map((tab) => (
81
- + <button
82
- + key={tab}
83
- + onClick={() => setFilter(tab)}
84
- + className={`px-5 py-2.5 text-sm bg-transparent border-0 -mb-px cursor-pointer outline-none transition-colors ${filter === tab
85
- + ? 'text-blue-500 font-semibold'
86
- + : 'text-gray-600 hover:text-gray-800'
87
- + }`}
88
- + >
89
- + {tab}
90
- + </button>
91
- + ))}
92
- + </div>
93
-
94
- <div className="space-y-3">
95
- {todos.map(todo => (
96
- <div
97
- key={todo.id}
98
- className="flex items-center justify-between bg-white px-4 py-3 rounded shadow-sm"
99
- >
100
- <div className="flex items-center gap-3 flex-1">
101
- <input
102
- type="checkbox"
103
- checked={todo.completed}
104
- onChange={() => toggleTodo(todo.id, todo.completed)}
105
- className="w-4 h-4 cursor-pointer"
106
- />
107
- <span className={`text-gray-700 ${todo.completed ? 'line-through text-gray-400' : ''}`}>
108
- {todo.text}
109
- </span>
110
- </div>
111
- <button
112
- onClick={() => deleteTodo(todo.id)}
113
- className="px-4 py-1 text-sm font-medium text-white bg-red-500 rounded hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition-colors"
114
- >
115
- Delete
116
- </button>
117
- </div>
118
- ))}
119
- </div>
120
-
121
- {todos.length === 0 && (
122
- <p className="text-center text-gray-400 mt-8">
123
- No todos yet. Add one above!
124
- </p>
125
- )}
126
- </div>
127
- </div>
128
- )
129
- }
130
-
131
- export default App
132
- ```
133
-
134
- If you run the app now, it'll look similar to this:
135
-
136
- ![](../../../assets/tutorial/chapter-6/0-tabbed-filter-element.png)
137
-
138
- You can switch between tabs and see how the tabbed component updates the currently active tab. Just like `input`, this uses local React state:
139
-
140
- ```ts
141
- const [filter, setFilter] = useState<'All' | 'Active' | 'Completed'>('All')
142
- ```
143
-
144
- This state is ephemeral, meaning it won't survive page refreshes. In a small app like this tutorial app, this won't really matter—but when you're building a complex UI where users will lose a lot of work when the state suddenly resets, being able to persist this state can be a real life-saver (including things like scroll positions, input forms, and any other relevant state that's important to your users)!
145
-
146
- With LiveStore, you can persist the state of the currently active tab across browser refreshes. Let's do it!
147
-
148
- ## Update the LiveStore schema with a client document
149
-
150
- In your `schema.ts` file, add another table definition to the `tables` object. This time, it won't be of type `State.SQLite.table` though, but rather use LiveStore's special [`clientDocument`](/api/livestore/livestore/namespaces/state/namespaces/sqlite/variables/clientdocument/) type for this:
151
-
152
- ```diff title="src/livestore/schema.ts" lang="ts"
153
- +import { Events, makeSchema, Schema, State, SessionIdSymbol } from '@livestore/livestore'
154
-
155
- +export const Filter = Schema.Literal('All', 'Active', 'Completed')
156
- +export type Filter = typeof Filter.Type
157
-
158
- export const tables = {
159
- todos: State.SQLite.table({
160
- name: 'todos',
161
- columns: {
162
- id: State.SQLite.integer({ primaryKey: true }),
163
- text: State.SQLite.text({ default: '' }),
164
- completed: State.SQLite.boolean({ default: false }),
165
- },
166
- }),
167
- + uiState: State.SQLite.clientDocument({
168
- + name: 'uiState',
169
- + schema: Schema.Struct({ input: Schema.String, filter: Filter }),
170
- + default: { id: SessionIdSymbol, value: { input: '', filter: 'All' } },
171
- + }),
172
- }
173
- ```
174
-
175
- On this table, you define:
176
-
177
- - A name for this client document.
178
- - The structure of the client document via `schema`; in your case:
179
- - The current state of the `input` text field for adding a new todo.
180
- - The `filter` that'll be used to filter the todos according to their `completed` status.
181
- - Default values for this client document.
182
-
183
- Unlike with other application state, you don't need to define custom events and materializers. The only thing you need to do is add the following event to your `events` object:
184
-
185
- ```diff title="src/livestore/schema.ts" lang="ts"
186
- export const events = {
187
- todoCreated: Events.synced({
188
- name: 'v1.TodoCreated',
189
- schema: Schema.Struct({ id: Schema.Number, text: Schema.String }),
190
- }),
191
- todoDeleted: Events.synced({
192
- name: 'v1.TodoDeleted',
193
- schema: Schema.Struct({ id: Schema.Number }),
194
- }),
195
- // Add these two events
196
- todoCompleted: Events.synced({
197
- name: 'v1.TodoCompleted',
198
- schema: Schema.Struct({ id: Schema.Number }),
199
- }),
200
- todoUncompleted: Events.synced({
201
- name: 'v1.TodoUncompleted',
202
- schema: Schema.Struct({ id: Schema.Number }),
203
- }),
204
- + uiStateSet: tables.uiState.set,
205
- }
206
- ```
207
-
208
- Here, both the event definition and materializer are automatically derived from the client document schema, with the materializer implementing upsert semantics.
209
-
210
- ## Implement local state with LiveStore
211
-
212
- In this step, you need to add the local state for the tabbed UI element to the `App` component. Additionally, you're going to replace the `useState` hook that you currently use for the `input` state with LiveStore's approach as well.
213
-
214
- Replace the `useState` usage for `input` and `filter` with LiveStore's [`useClientDocument`](/framework-integrations/react-integration#useclientdocument):
215
-
216
- ```diff title="src/App.tsx" lang="tsx"
217
- +const [{ input, filter }, setUiState, , uiState$] = store.useClientDocument(tables.uiState)
218
-
219
- +const updatedInput = (input: string) => setUiState((state) => ({ ...state, input }))
220
- +const updatedFilter = (filter: Filter) => setUiState((state) => ({ ...state, filter }))
221
- -const [input, setInput] = useState('')
222
- -const [filter, setFilter] = useState<'All' | 'Active' | 'Completed'>('All')
223
- -const updatedInput = (input: string) => store.commit(events.uiStateSet({ input }))
224
- -const updatedFilter = (filter: Filter) => store.commit(events.uiStateSet({ filter }))
225
- ```
226
-
227
- Don't forget to update the imports to include the `Filter` type and drop `useState`:
228
-
229
- ```diff title="src/App.tsx" lang="tsx"
230
- -import { useState } from 'react'
231
-
232
- ```
233
-
234
- `useClientDocument` persists the UI state inside LiveStore while giving you a React-friendly setter function similar to `useState`.
235
-
236
- Since you've renamed the functions to update the values of `input` and `filter` you need to adjust the parts of the `App` component where they are used:
237
-
238
- - `setInput` → `updatedInput`
239
- - `setFilter` → `updatedFilter`
240
-
241
- You have now recreated the same functionality from before and are able to switch the tabs in the UI element—with one important difference: If you refresh the browser, the UI state will remain the same as before. Your UI state is now persisted and survives page refreshes:
242
-
243
- ![](../../../assets/tutorial/chapter-6/1-persist-ui-state.gif)
244
-
245
- ## Implement filter logic
246
-
247
- The last step in this tutorial is to actually update the list based on which tab is currently selected.
248
-
249
- With all your current knowledge, you could think that the implementation would need to look something like this:
250
-
251
- ```tsx title="src/App.tsx"
252
- const todos$ = queryDb(() => tables.todos.where({
253
- completed:
254
- filter === 'Completed' ? true
255
- : filter === 'Active' ? false
256
- : undefined // if `undefined` is passed to `where`, no filtering happens
257
- }))
258
- const todos = store.useQuery(todos$)
259
- ```
260
-
261
- If you try that out though, you'll notice that this works _once_ (when your browser loads for the first time). However, when you switch tabs, the list will not actually update.
262
-
263
- That's because the query isn't updated with the new value `filter` value when it changes. Here's how you need to do it instead:
264
-
265
- ```tsx title="src/App.tsx"
266
- const todos$ = queryDb((
267
- (get) => {
268
- const { filter } = get(uiState$)
269
- return tables.todos.where({
270
- completed: filter === 'Completed' ? true
271
- : filter === 'Active' ? false
272
- : undefined
273
- })
274
- }
275
- ), { label: 'todos' })
276
- const todos = store.useQuery(todos$)
277
- ```
278
-
279
- Here, `uiState$` comes from `useClientDocument`, ensuring that the todos query automatically reacts whenever the persisted UI state changes.
280
-
281
- This is the final code for the `App` component:
282
-
283
- ```tsx title="src/App.tsx"
284
-
285
- function App() {
286
-
287
- const { store } = useStore()
288
-
289
- const [{ input, filter }, setUiState, , uiState$] = store.useClientDocument(tables.uiState)
290
-
291
- const todos$ = queryDb(
292
- (get) => {
293
- const { filter } = get(uiState$)
294
- return tables.todos.where({
295
- completed: filter === 'Completed' ? true
296
- : filter === 'Active' ? false
297
- : undefined
298
- })
299
- },
300
- { label: 'todos' },
301
- )
302
- const todos = store.useQuery(todos$)
303
-
304
- const updatedInput = (input: string) => setUiState((state) => ({ ...state, input }))
305
- const updatedFilter = (filter: Filter) => setUiState((state) => ({ ...state, filter }))
306
-
307
- const addTodo = () => {
308
- if (input.trim()) {
309
- store.commit(
310
- events.todoCreated({ id: Date.now(), text: input }),
311
- )
312
- updatedInput('')
313
- }
314
- }
315
-
316
- const deleteTodo = (id: number) => {
317
- store.commit(
318
- events.todoDeleted({ id }),
319
- )
320
- }
321
-
322
- const toggleTodo = (id: number, completed: boolean) => {
323
- store.commit(
324
- completed ? events.todoUncompleted({ id }) : events.todoCompleted({ id })
325
- )
326
- }
327
-
328
- const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
329
- if (e.key === 'Enter') {
330
- addTodo()
331
- }
332
- }
333
-
334
- return (
335
- <div className="min-h-screen bg-gray-50 flex items-start justify-center p-6">
336
- <div className="w-full max-w-lg">
337
- <h1 className="text-5xl font-bold text-gray-800 text-center mb-12">
338
- Todo List
339
- </h1>
340
-
341
- <div className="flex gap-3 mb-8">
342
- <input
343
- type="text"
344
- value={input}
345
- onChange={(e) => updatedInput(e.target.value)}
346
- onKeyDown={handleKeyDown}
347
- placeholder="Enter a todo..."
348
- className="flex-1 px-4 py-2 text-sm border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
349
- />
350
- <button
351
- onClick={addTodo}
352
- className="px-6 py-2 text-sm font-medium text-white bg-blue-500 rounded hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
353
- >
354
- Add
355
- </button>
356
- </div>
357
-
358
- <div className="flex gap-1 mb-4 border-b border-gray-200 justify-center">
359
- {(['All', 'Active', 'Completed'] as const).map((tab) => (
360
- <button
361
- key={tab}
362
- onClick={() => updatedFilter(tab)}
363
- className={`px-5 py-2.5 text-sm bg-transparent border-0 -mb-px cursor-pointer outline-none transition-colors ${filter === tab
364
- ? 'text-blue-500 font-semibold'
365
- : 'text-gray-600 hover:text-gray-800'
366
- }`}
367
- >
368
- {tab}
369
- </button>
370
- ))}
371
- </div>
372
-
373
- <div className="space-y-3 min-h-[200px]">
374
- {todos.map(todo => (
375
- <div
376
- key={todo.id}
377
- className="flex items-center justify-between bg-white px-4 py-3 rounded shadow-sm"
378
- >
379
- <div className="flex items-center gap-3 flex-1">
380
- <input
381
- type="checkbox"
382
- checked={todo.completed}
383
- onChange={() => toggleTodo(todo.id, todo.completed)}
384
- className="w-4 h-4 cursor-pointer"
385
- />
386
- <span className={`text-gray-700 ${todo.completed ? 'line-through text-gray-400' : ''}`}>
387
- {todo.text}
388
- </span>
389
- </div>
390
- <button
391
- onClick={() => deleteTodo(todo.id)}
392
- className="px-4 py-1 text-sm font-medium text-white bg-red-500 rounded hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition-colors"
393
- >
394
- Delete
395
- </button>
396
- </div>
397
- ))}
398
- </div>
399
-
400
- {todos.length === 0 && (
401
- <p className="text-center text-gray-400 mt-8">
402
- No todos yet. Add one above!
403
- </p>
404
- )}
405
- </div>
406
- </div>
407
- )
408
- }
409
-
410
- export default App
411
- ```
412
-
413
- ## Test the app
414
-
415
- You can test the app by running the `dev` script:
416
-
417
- <Tabs syncKey="package-manager">
418
-
419
- <TabItem label="bun">
420
-
421
- <Code code={`bun dev`} lang="sh" />
422
-
423
- </TabItem>
424
-
425
- <TabItem label="pnpm">
426
-
427
- <Code code={`pnpm dev`} lang="sh" />
428
-
429
- </TabItem>
430
-
431
- </Tabs>
432
-
433
- The filters now will be applied and update the list of todos when changed:
434
-
435
- ![](../../../assets/tutorial/chapter-6/2-final-app.gif)
436
-
437
- You can observe the same behaviour if you deploy the app using the `deploy` script:
438
-
439
- <Tabs syncKey="package-manager">
440
-
441
- <TabItem label="bun">
442
-
443
- <Code code={`bun run deploy`} lang="sh" />
444
-
445
- </TabItem>
446
-
447
- <TabItem label="pnpm">
448
-
449
- <Code code={`pnpm run deploy`} lang="sh" />
450
-
451
- </TabItem>
452
-
453
- </Tabs>
@@ -1,22 +0,0 @@
1
- # Next steps
2
-
3
- This tutorial walked you through the process of building a basic application with LiveStore, explaining its fundamental concepts and workflows by practical example.
4
-
5
- ## Next steps
6
-
7
- Try one of our starter projects:
8
-
9
- <LinkButton href="/getting-started/react-web">Quickstart</LinkButton>
10
-
11
- Follow LiveStore on social media to never miss an update:
12
-
13
- <LinkButton href="https://x.com/livestoredev">Follow on X</LinkButton>
14
- <LinkButton href="https://bsky.app/profile/livestoredev.bsky.social">Follow on Bluesky</LinkButton>
15
-
16
- Join the LiveStore community and chat with other developers:
17
-
18
- <LinkButton href="https://discord.gg/RbMcjUAPd7/">Join Discord</LinkButton>
19
-
20
- ## Credits
21
-
22
- This tutorial has been written by [Nikolas Burk](https://x.com/nikolasburk).
@@ -1,33 +0,0 @@
1
- # Design decisions
2
-
3
- ## Goals
4
-
5
- - Fast, synchronous, transactional, and reactive state management
6
- - Global state is eventually consistent
7
- - Persistent storage
8
- - Syncing
9
- - Convenient schema migrations
10
- - Great devtools
11
-
12
- ## Major Design Decisions
13
-
14
- - Based on [event-sourcing](/understanding-livestore/event-sourcing) (implying a read/write model separation)
15
- - Using SQLite for state management over JavaScript implementations
16
- - There are many benefits to using SQLite for state management, including performance, reliability, and ease of use.
17
- - Run in-memory SQLite in main-thread to enable synchronous queries
18
- - Usually LiveStore is used with a second SQLite database for persistence running in a separate thread (e.g. web worker)
19
- - Running SQLite additionally in the main-thread however also means each tab uses extra memory.
20
- - The current implementation of LiveStore assumes that the data is small enough to fit in memory. However, SQLite is very efficient so this should work for many use cases and apps.
21
- - LiveStore implements a Signals-based reactivity system based on the ideas of Adapton for incremental computation
22
- - The goal is to keep LiveStore syncing provider agnostic so you can use the right syncing provider for your use case.
23
- - LiveStore intentionally stays focused on core data management and syncing, leaving concerns like authentication, file uploads, and business logic to application code. This minimalist approach keeps the library maintainable by limiting surface area, flexible as a composable Unix-like building block, and unopinionated enough to adapt to diverse usage scenarios.
24
-
25
- ## Implementation decisions
26
-
27
- - Build most of the library in TypeScript. We might move more parts to Rust in the future.
28
- - Embrace and build on top of [Effect](https://effect.website) as a library of powerful primitives, particularly for IO/concurrency heavy parts of the library.
29
-
30
- ## Original motivation
31
-
32
- - Frustration with database schema migrations -> event sourcing to separate read and write model (avoid schema migrations for read model)
33
- - Applying the "Make the right thing easy" principle to app data management
@@ -1,40 +0,0 @@
1
- # Event sourcing
2
-
3
- - Similar to Redux but persisted and synced across devices
4
- - Provides a more principled way to handle data instead of relying on mutable state
5
- - Core idea: Separate read vs write model
6
- - Read model: App database (i.e. SQLite)
7
- - Write model: Ordered log of all mutation events
8
- - Related topics
9
- - Domain driven design
10
- - Benefits
11
- - Simple mental model
12
- - Preserves user intent
13
- - Scalable
14
- - Flexible
15
- - You can easily evolve the read model based on your query patterns as your app requirements change over time
16
- - Flexible merge conflicts resolution
17
- - Automatic migrations of the read model (i.e. app database)
18
- - Write model can also be evolved (e.g. via versioned mutations and optionally mapping old mutations to new ones)
19
- - History of all state changes is captured (e.g. for auditing and debugging)
20
- - Foundation for syncing
21
- - Downsides
22
- - Slightly more boilerplate to manually define mutations
23
- - Need to be careful so eventlog doesn't grow too much
24
-
25
- ## LiveStore as an event-sourcing framework
26
-
27
- While the benefits of event sourcing are compelling, building a robust system from scratch is complex and time-consuming. Developers often encounter pitfalls related to data consistency, schema migrations, and efficient state reconstruction.
28
-
29
- LiveStore provides an off-the-shelf event sourcing solution designed for ease of use and correctness. It simplifies development by:
30
-
31
- - Providing clear APIs for defining mutations (events).
32
- - Automatically managing the event log persistence and ordering.
33
- - Efficiently recomputing the state (e.g. SQLite database) from the eventlog via materializers.
34
- - Handling complexities like automatic data migrations and offering strategies for conflict resolution during synchronization.
35
-
36
- This allows you to leverage the power of event sourcing without needing to implement the underlying infrastructure and tackle common edge cases yourself.
37
-
38
- ## Further reading
39
-
40
- - [The Log: What every software engineer should know about real-time data's unifying abstraction](https://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying)