@livestore/react 0.4.0-dev.9 → 0.5.0-dev.0

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 (85) hide show
  1. package/README.md +1 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/StoreRegistryContext.d.ts +56 -0
  4. package/dist/StoreRegistryContext.d.ts.map +1 -0
  5. package/dist/StoreRegistryContext.js +61 -0
  6. package/dist/StoreRegistryContext.js.map +1 -0
  7. package/dist/__tests__/fixture.d.ts +12 -283
  8. package/dist/__tests__/fixture.d.ts.map +1 -1
  9. package/dist/__tests__/fixture.js +12 -86
  10. package/dist/__tests__/fixture.js.map +1 -1
  11. package/dist/experimental/components/LiveList.d.ts +4 -2
  12. package/dist/experimental/components/LiveList.d.ts.map +1 -1
  13. package/dist/experimental/components/LiveList.js +11 -9
  14. package/dist/experimental/components/LiveList.js.map +1 -1
  15. package/dist/mod.d.ts +8 -5
  16. package/dist/mod.d.ts.map +1 -1
  17. package/dist/mod.js +6 -4
  18. package/dist/mod.js.map +1 -1
  19. package/dist/useClientDocument.d.ts +12 -4
  20. package/dist/useClientDocument.d.ts.map +1 -1
  21. package/dist/useClientDocument.js +4 -18
  22. package/dist/useClientDocument.js.map +1 -1
  23. package/dist/useClientDocument.test.js +28 -20
  24. package/dist/useClientDocument.test.js.map +1 -1
  25. package/dist/useQuery.d.ts +29 -8
  26. package/dist/useQuery.d.ts.map +1 -1
  27. package/dist/useQuery.js +43 -70
  28. package/dist/useQuery.js.map +1 -1
  29. package/dist/useQuery.test.js +67 -23
  30. package/dist/useQuery.test.js.map +1 -1
  31. package/dist/useRcResource.d.ts +1 -1
  32. package/dist/useRcResource.d.ts.map +1 -1
  33. package/dist/useRcResource.js +42 -35
  34. package/dist/useRcResource.js.map +1 -1
  35. package/dist/useRcResource.test.js +73 -50
  36. package/dist/useRcResource.test.js.map +1 -1
  37. package/dist/useStore.d.ts +74 -7
  38. package/dist/useStore.d.ts.map +1 -1
  39. package/dist/useStore.js +82 -16
  40. package/dist/useStore.js.map +1 -1
  41. package/dist/useStore.test.d.ts +2 -0
  42. package/dist/useStore.test.d.ts.map +1 -0
  43. package/dist/useStore.test.js +242 -0
  44. package/dist/useStore.test.js.map +1 -0
  45. package/dist/useSyncStatus.d.ts +22 -0
  46. package/dist/useSyncStatus.d.ts.map +1 -0
  47. package/dist/useSyncStatus.js +28 -0
  48. package/dist/useSyncStatus.js.map +1 -0
  49. package/package.json +60 -27
  50. package/src/StoreRegistryContext.tsx +70 -0
  51. package/src/__snapshots__/useClientDocument.test.tsx.snap +152 -78
  52. package/src/__snapshots__/useQuery.test.tsx.snap +12 -12
  53. package/src/__tests__/fixture.tsx +32 -135
  54. package/src/experimental/components/LiveList.tsx +25 -11
  55. package/src/mod.ts +8 -12
  56. package/src/useClientDocument.test.tsx +38 -23
  57. package/src/useClientDocument.ts +19 -38
  58. package/src/useQuery.test.tsx +110 -23
  59. package/src/useQuery.ts +74 -89
  60. package/src/useRcResource.test.tsx +116 -59
  61. package/src/useRcResource.ts +52 -38
  62. package/src/useStore.test.tsx +357 -0
  63. package/src/useStore.ts +115 -22
  64. package/src/useSyncStatus.ts +34 -0
  65. package/dist/LiveStoreContext.d.ts +0 -13
  66. package/dist/LiveStoreContext.d.ts.map +0 -1
  67. package/dist/LiveStoreContext.js +0 -3
  68. package/dist/LiveStoreContext.js.map +0 -1
  69. package/dist/LiveStoreProvider.d.ts +0 -65
  70. package/dist/LiveStoreProvider.d.ts.map +0 -1
  71. package/dist/LiveStoreProvider.js +0 -221
  72. package/dist/LiveStoreProvider.js.map +0 -1
  73. package/dist/LiveStoreProvider.test.d.ts +0 -2
  74. package/dist/LiveStoreProvider.test.d.ts.map +0 -1
  75. package/dist/LiveStoreProvider.test.js +0 -117
  76. package/dist/LiveStoreProvider.test.js.map +0 -1
  77. package/dist/utils/stack-info.d.ts +0 -4
  78. package/dist/utils/stack-info.d.ts.map +0 -1
  79. package/dist/utils/stack-info.js +0 -10
  80. package/dist/utils/stack-info.js.map +0 -1
  81. package/src/LiveStoreContext.ts +0 -14
  82. package/src/LiveStoreProvider.test.tsx +0 -248
  83. package/src/LiveStoreProvider.tsx +0 -413
  84. package/src/ambient.d.ts +0 -1
  85. package/src/utils/stack-info.ts +0 -13
@@ -1,131 +1,45 @@
1
- import { makeInMemoryAdapter } from '@livestore/adapter-web'
2
- import { provideOtel, type UnexpectedError } from '@livestore/common'
3
- import { Events, makeSchema, State } from '@livestore/common/schema'
4
- import type { LiveStoreSchema, SqliteDsl, Store } from '@livestore/livestore'
5
- import { createStore } from '@livestore/livestore'
6
- import { omitUndefineds } from '@livestore/utils'
7
- import { Effect, Schema, type Scope } from '@livestore/utils/effect'
8
- import type * as otel from '@opentelemetry/api'
9
1
  import React from 'react'
10
2
 
11
- import * as LiveStoreReact from '../mod.js'
12
-
13
- export type Todo = {
14
- id: string
15
- text: string
16
- completed: boolean
3
+ import type { UnknownError } from '@livestore/common'
4
+ import {
5
+ type AppState,
6
+ type CreateTodoMvcStoreOptions,
7
+ createTodoMvcStore,
8
+ events,
9
+ type Filter,
10
+ schema,
11
+ type Todo,
12
+ tables,
13
+ } from '@livestore/framework-toolkit/testing'
14
+ import type { Store } from '@livestore/livestore'
15
+ import { Effect, type Scope } from '@livestore/utils/effect'
16
+
17
+ import * as LiveStoreReact from '../mod.ts'
18
+
19
+ export type { AppState, Filter, Todo }
20
+ // Re-export shared types and schema
21
+ export { events, schema, tables }
22
+
23
+ export type MakeTodoMvcReactOptions = CreateTodoMvcStoreOptions & {
24
+ strictMode?: boolean | undefined
17
25
  }
18
26
 
19
- export type Filter = 'all' | 'active' | 'completed'
20
-
21
- export type AppState = {
22
- newTodoText: string
23
- filter: Filter
24
- }
25
-
26
- const todos = State.SQLite.table({
27
- name: 'todos',
28
- columns: {
29
- id: State.SQLite.text({ primaryKey: true }),
30
- text: State.SQLite.text({ default: '', nullable: false }),
31
- completed: State.SQLite.boolean({ default: false, nullable: false }),
32
- },
33
- })
34
-
35
- const app = State.SQLite.table({
36
- name: 'app',
37
- columns: {
38
- id: State.SQLite.text({ primaryKey: true, default: 'static' }),
39
- newTodoText: State.SQLite.text({ default: '', nullable: true }),
40
- filter: State.SQLite.text({ default: 'all', nullable: false }),
41
- },
42
- })
43
-
44
- const userInfo = State.SQLite.clientDocument({
45
- name: 'UserInfo',
46
- schema: Schema.Struct({
47
- username: Schema.String,
48
- text: Schema.String,
49
- }),
50
- default: { value: { username: '', text: '' } },
51
- })
52
-
53
- const AppRouterSchema = State.SQLite.clientDocument({
54
- name: 'AppRouter',
55
- schema: Schema.Struct({
56
- currentTaskId: Schema.String.pipe(Schema.NullOr),
57
- }),
58
- default: {
59
- value: { currentTaskId: null },
60
- id: 'singleton',
61
- },
62
- })
63
-
64
- const kv = State.SQLite.clientDocument({
65
- name: 'Kv',
66
- schema: Schema.Any,
67
- default: { value: null },
68
- })
69
-
70
- export const events = {
71
- todoCreated: Events.synced({
72
- name: 'todoCreated',
73
- schema: Schema.Struct({ id: Schema.String, text: Schema.String, completed: Schema.Boolean }),
74
- }),
75
- todoUpdated: Events.synced({
76
- name: 'todoUpdated',
77
- schema: Schema.Struct({
78
- id: Schema.String,
79
- text: Schema.String.pipe(Schema.optional),
80
- completed: Schema.Boolean.pipe(Schema.optional),
81
- }),
82
- }),
83
- AppRouterSet: AppRouterSchema.set,
84
- UserInfoSet: userInfo.set,
85
- KvSet: kv.set,
86
- }
87
-
88
- const materializers = State.SQLite.materializers(events, {
89
- todoCreated: ({ id, text, completed }) => todos.insert({ id, text, completed }),
90
- todoUpdated: ({ id, text, completed }) => todos.update({ ...omitUndefineds({ completed, text }) }).where({ id }),
91
- })
92
-
93
- export const tables = { todos, app, userInfo, AppRouterSchema, kv }
94
-
95
- const state = State.SQLite.makeState({ tables, materializers })
96
- export const schema = makeSchema({ state, events })
97
-
98
- export const makeTodoMvcReact: ({
99
- otelTracer,
100
- otelContext,
101
- strictMode,
102
- }?: {
103
- otelTracer?: otel.Tracer
104
- otelContext?: otel.Context
105
- strictMode?: boolean
106
- }) => Effect.Effect<
27
+ export const makeTodoMvcReact: (opts?: MakeTodoMvcReactOptions) => Effect.Effect<
107
28
  {
108
29
  wrapper: ({ children }: any) => React.JSX.Element
109
- store: Store<LiveStoreSchema<SqliteDsl.DbSchema, State.SQLite.EventDefRecord>, {}> & LiveStoreReact.ReactApi
30
+ store: Store<typeof schema> & LiveStoreReact.ReactApi
110
31
  renderCount: { readonly val: number; inc: () => void }
111
32
  },
112
- UnexpectedError,
33
+ UnknownError,
113
34
  Scope.Scope
114
- > = ({
115
- otelTracer,
116
- otelContext,
117
- strictMode,
118
- }: {
119
- otelTracer?: otel.Tracer
120
- otelContext?: otel.Context
121
- strictMode?: boolean
122
- } = {}) =>
35
+ > = (opts: MakeTodoMvcReactOptions = {}) =>
123
36
  Effect.gen(function* () {
37
+ const { strictMode } = opts
124
38
  const makeRenderCount = () => {
125
39
  let val = 0
126
40
 
127
41
  const inc = () => {
128
- val += strictMode ? 0.5 : 1
42
+ val += strictMode === true ? 0.5 : 1
129
43
  }
130
44
 
131
45
  return {
@@ -136,32 +50,15 @@ export const makeTodoMvcReact: ({
136
50
  }
137
51
  }
138
52
 
139
- const store: Store<any> = yield* createStore({
140
- schema,
141
- storeId: 'default',
142
- adapter: makeInMemoryAdapter(),
143
- debug: { instanceId: 'test' },
144
- })
53
+ const store = yield* createTodoMvcStore(opts)
145
54
 
146
55
  const storeWithReactApi = LiveStoreReact.withReactApi(store)
147
56
 
148
- // TODO improve typing of `LiveStoreContext`
149
- const storeContext = {
150
- stage: 'running' as const,
151
- store: storeWithReactApi,
152
- }
153
-
154
- const MaybeStrictMode = strictMode ? React.StrictMode : React.Fragment
57
+ const MaybeStrictMode = strictMode === true ? React.StrictMode : React.Fragment
155
58
 
156
- const wrapper = ({ children }: any) => (
157
- <MaybeStrictMode>
158
- <LiveStoreReact.LiveStoreContext.Provider value={storeContext}>
159
- {children}
160
- </LiveStoreReact.LiveStoreContext.Provider>
161
- </MaybeStrictMode>
162
- )
59
+ const wrapper = ({ children }: any) => <MaybeStrictMode>{children}</MaybeStrictMode>
163
60
 
164
61
  const renderCount = makeRenderCount()
165
62
 
166
63
  return { wrapper, store: storeWithReactApi, renderCount }
167
- }).pipe(provideOtel(omitUndefineds({ parentSpanContext: otelContext, otelTracer })))
64
+ })
@@ -1,8 +1,9 @@
1
- import type { LiveQueryDef } from '@livestore/livestore'
2
- import { computed } from '@livestore/livestore'
3
1
  import React from 'react'
4
2
 
5
- import { useQuery } from '../../useQuery.js'
3
+ import type { LiveQueryDef, Store } from '@livestore/livestore'
4
+ import { computed } from '@livestore/livestore'
5
+
6
+ import { useQuery } from '../../useQuery.ts'
6
7
 
7
8
  /*
8
9
  TODO:
@@ -16,6 +17,8 @@ export type LiveListProps<TItem> = {
16
17
  renderItem: (item: TItem, opts: { index: number; isInitialListRender: boolean }) => React.ReactNode
17
18
  /** Needs to be unique across all list items */
18
19
  getKey: (item: TItem, index: number) => string | number
20
+ /** The store instance to use for queries */
21
+ store: Store<any, any>
19
22
  }
20
23
 
21
24
  /**
@@ -26,12 +29,15 @@ export type LiveListProps<TItem> = {
26
29
  * In the future we want to make this component even more efficient by using incremental rendering (https://github.com/livestorejs/livestore/pull/55)
27
30
  * e.g. when an item is added/removed/moved to only re-render the affected DOM nodes.
28
31
  */
29
- export const LiveList = <TItem,>({ items$, renderItem, getKey }: LiveListProps<TItem>): React.ReactNode => {
32
+ export const LiveList = <TItem,>({ items$, renderItem, getKey, store }: LiveListProps<TItem>): React.ReactNode => {
30
33
  const [hasMounted, setHasMounted] = React.useState(false)
31
34
 
32
35
  React.useEffect(() => setHasMounted(true), [])
33
36
 
34
- const keys = useQuery(computed((get) => get(items$).map(getKey)))
37
+ const keys = useQuery(
38
+ computed((get) => get(items$).map(getKey)),
39
+ { store },
40
+ )
35
41
  const arr = React.useMemo(
36
42
  () =>
37
43
  keys.map(
@@ -54,7 +60,9 @@ export const LiveList = <TItem,>({ items$, renderItem, getKey }: LiveListProps<T
54
60
  key={key}
55
61
  itemKey={key}
56
62
  item$={item$}
57
- opts={{ isInitialListRender: !hasMounted, index }}
63
+ store={store}
64
+ index={index}
65
+ isInitialListRender={!hasMounted}
58
66
  renderItem={renderItem}
59
67
  />
60
68
  ))}
@@ -64,15 +72,20 @@ export const LiveList = <TItem,>({ items$, renderItem, getKey }: LiveListProps<T
64
72
 
65
73
  const ItemWrapper = <TItem,>({
66
74
  item$,
67
- opts,
75
+ index,
76
+ isInitialListRender,
68
77
  renderItem,
78
+ store,
69
79
  }: {
70
80
  itemKey: string | number
71
81
  item$: LiveQueryDef<TItem>
72
- opts: { index: number; isInitialListRender: boolean }
82
+ index: number
83
+ isInitialListRender: boolean
73
84
  renderItem: (item: TItem, opts: { index: number; isInitialListRender: boolean }) => React.ReactNode
85
+ store: Store<any, any>
74
86
  }) => {
75
- const item = useQuery(item$)
87
+ const item = useQuery(item$, { store })
88
+ const opts = React.useMemo(() => ({ index, isInitialListRender }), [index, isInitialListRender])
76
89
 
77
90
  return <>{renderItem(item, opts)}</>
78
91
  }
@@ -82,6 +95,7 @@ const ItemWrapperMemo = React.memo(
82
95
  (prev, next) =>
83
96
  prev.itemKey === next.itemKey &&
84
97
  prev.renderItem === next.renderItem &&
85
- prev.opts.index === next.opts.index &&
86
- prev.opts.isInitialListRender === next.opts.isInitialListRender,
98
+ prev.store === next.store &&
99
+ prev.index === next.index &&
100
+ prev.isInitialListRender === next.isInitialListRender,
87
101
  ) as typeof ItemWrapper
package/src/mod.ts CHANGED
@@ -1,13 +1,9 @@
1
- export { LiveStoreContext, type ReactApi } from './LiveStoreContext.ts'
2
- export { LiveStoreProvider } from './LiveStoreProvider.tsx'
3
- export {
4
- type Dispatch,
5
- type SetStateAction,
6
- type SetStateActionPartial,
7
- type StateSetters,
8
- type UseClientDocumentResult,
9
- useClientDocument,
10
- } from './useClientDocument.ts'
1
+ export type { Dispatch, SetStateAction, SetStateActionPartial, StateSetters } from '@livestore/framework-toolkit'
2
+ export { captureStackInfo } from '@livestore/framework-toolkit'
3
+ export { StoreRegistry, storeOptions } from '@livestore/livestore'
4
+ export { LiveList, type LiveListProps } from './experimental/components/LiveList.tsx'
5
+ export * from './StoreRegistryContext.tsx'
6
+ export { type UseClientDocumentResult, useClientDocument } from './useClientDocument.ts'
11
7
  export { useQuery, useQueryRef } from './useQuery.ts'
12
- export { useStore, withReactApi } from './useStore.ts'
13
- export { useStackInfo } from './utils/stack-info.ts'
8
+ export { type ReactApi, useStore, withReactApi } from './useStore.ts'
9
+ export { useSyncStatus } from './useSyncStatus.ts'
@@ -1,28 +1,29 @@
1
- /** biome-ignore-all lint/a11y/useValidAriaRole: not needed for testing */
2
- /** biome-ignore-all lint/a11y/noStaticElementInteractions: not needed for testing */
3
- import * as LiveStore from '@livestore/livestore'
4
- import { getAllSimplifiedRootSpans, getSimplifiedRootSpan } from '@livestore/livestore/internal/testing-utils'
5
- import { Effect, ReadonlyRecord, Schema } from '@livestore/utils/effect'
6
- import { Vitest } from '@livestore/utils-dev/node-vitest'
7
1
  import * as otel from '@opentelemetry/api'
8
2
  import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
9
3
  import * as ReactTesting from '@testing-library/react'
10
- import type React from 'react'
4
+ import * as React from 'react'
11
5
  import { beforeEach, expect, it } from 'vitest'
12
6
 
13
- import { events, makeTodoMvcReact, tables } from './__tests__/fixture.js'
14
- import type * as LiveStoreReact from './mod.js'
15
- import { __resetUseRcResourceCache } from './useRcResource.js'
7
+ import * as LiveStore from '@livestore/livestore'
8
+ import { StoreInternalsSymbol } from '@livestore/livestore'
9
+ import { getAllSimplifiedRootSpans, getSimplifiedRootSpan } from '@livestore/livestore/internal/testing-utils'
10
+ import { Vitest } from '@livestore/utils-dev/node-vitest'
11
+ import { Effect, ReadonlyRecord, Schema } from '@livestore/utils/effect'
12
+
13
+ import { events, makeTodoMvcReact, tables } from './__tests__/fixture.tsx'
14
+ import type * as LiveStoreReact from './mod.ts'
15
+ import { __resetUseRcResourceCache } from './useRcResource.ts'
16
16
 
17
17
  // const strictMode = process.env.REACT_STRICT_MODE !== undefined
18
18
 
19
19
  // NOTE running tests concurrently doesn't work with the default global db graph
20
+ /** Verifies: LS.SYS.INT.REACT-R01, LS.SYS.STATE.SQLITE-R07 */
20
21
  Vitest.describe('useClientDocument', () => {
21
22
  beforeEach(() => {
22
23
  __resetUseRcResourceCache()
23
24
  })
24
25
 
25
- Vitest.scopedLive('should update the data based on component key', () =>
26
+ Vitest.live('should update the data based on component key', () =>
26
27
  Effect.gen(function* () {
27
28
  const { wrapper, store, renderCount } = yield* makeTodoMvcReact({})
28
29
 
@@ -39,12 +40,12 @@ Vitest.describe('useClientDocument', () => {
39
40
  expect(result.current.id).toBe('u1')
40
41
  expect(result.current.state.username).toBe('')
41
42
  expect(renderCount.val).toBe(1)
42
- expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
43
+ expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
43
44
  store.commit(tables.userInfo.set({ username: 'username_u2' }, 'u2'))
44
45
 
45
46
  rerender('u2')
46
47
 
47
- expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
48
+ expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
48
49
  expect(result.current.id).toBe('u2')
49
50
  expect(result.current.state.username).toBe('username_u2')
50
51
  expect(renderCount.val).toBe(2)
@@ -53,7 +54,7 @@ Vitest.describe('useClientDocument', () => {
53
54
 
54
55
  // TODO add a test that makes sure React doesn't re-render when a setter is used to set the same value
55
56
 
56
- Vitest.scopedLive('should update the data reactively - via setState', () =>
57
+ Vitest.live('should update the data reactively - via setState', () =>
57
58
  Effect.gen(function* () {
58
59
  const { wrapper, store, renderCount } = yield* makeTodoMvcReact({})
59
60
 
@@ -79,7 +80,7 @@ Vitest.describe('useClientDocument', () => {
79
80
  }),
80
81
  )
81
82
 
82
- Vitest.scopedLive('should update the data reactively - via raw store commit', () =>
83
+ Vitest.live('should update the data reactively - via raw store commit', () =>
83
84
  Effect.gen(function* () {
84
85
  const { wrapper, store, renderCount } = yield* makeTodoMvcReact({})
85
86
 
@@ -105,7 +106,7 @@ Vitest.describe('useClientDocument', () => {
105
106
  }),
106
107
  )
107
108
 
108
- Vitest.scopedLive('should work for a larger app', () =>
109
+ Vitest.live('should work for a larger app', () =>
109
110
  Effect.gen(function* () {
110
111
  const { wrapper, store, renderCount } = yield* makeTodoMvcReact({})
111
112
 
@@ -119,25 +120,39 @@ Vitest.describe('useClientDocument', () => {
119
120
  renderCount.inc()
120
121
 
121
122
  const [state, setState] = store.useClientDocument(tables.AppRouterSchema, 'singleton')
123
+ const setCurrentTaskId = React.useCallback((taskId: string) => setState({ currentTaskId: taskId }), [setState])
122
124
 
123
125
  globalSetState = setState
124
126
 
125
127
  return (
126
128
  <div>
127
- <TasksList setTaskId={(taskId) => setState({ currentTaskId: taskId })} />
129
+ <TasksList setTaskId={setCurrentTaskId} />
128
130
  <div role="current-id">Current Task Id: {state.currentTaskId ?? '-'}</div>
129
- {state.currentTaskId ? <TaskDetails id={state.currentTaskId} /> : <div>Click on a task to see details</div>}
131
+ {state.currentTaskId !== null ? (
132
+ <TaskDetails id={state.currentTaskId} />
133
+ ) : (
134
+ <div>Click on a task to see details</div>
135
+ )}
130
136
  </div>
131
137
  )
132
138
  }
133
139
 
134
140
  const TasksList: React.FC<{ setTaskId: (_: string) => void }> = ({ setTaskId }) => {
135
141
  const allTodos = store.useQuery(allTodos$)
142
+ const handleTaskClick = React.useCallback(
143
+ (event: React.MouseEvent<HTMLDivElement>) => {
144
+ const taskId = event.currentTarget.dataset.taskId
145
+ if (taskId !== undefined) {
146
+ setTaskId(taskId)
147
+ }
148
+ },
149
+ [setTaskId],
150
+ )
136
151
 
137
152
  return (
138
153
  <div>
139
154
  {allTodos.map((_) => (
140
- <div key={_.id} onClick={() => setTaskId(_.id)}>
155
+ <div key={_.id} data-task-id={_.id} onClick={handleTaskClick}>
141
156
  {_.id}
142
157
  </div>
143
158
  ))}
@@ -181,7 +196,7 @@ Vitest.describe('useClientDocument', () => {
181
196
  }),
182
197
  )
183
198
 
184
- Vitest.scopedLive('should work for a useClientDocument query chained with a useTemporary query', () =>
199
+ Vitest.live('should work for a useClientDocument query chained with a useTemporary query', () =>
185
200
  Effect.gen(function* () {
186
201
  const { store, wrapper, renderCount } = yield* makeTodoMvcReact({})
187
202
 
@@ -224,7 +239,7 @@ Vitest.describe('useClientDocument', () => {
224
239
  }),
225
240
  )
226
241
 
227
- Vitest.scopedLive('kv client document overwrites value (Schema.Any, no partial merge)', () =>
242
+ Vitest.live('kv client document overwrites value (Schema.Any, no partial merge)', () =>
228
243
  Effect.gen(function* () {
229
244
  const { wrapper, store, renderCount } = yield* makeTodoMvcReact({})
230
245
 
@@ -262,7 +277,7 @@ Vitest.describe('useClientDocument', () => {
262
277
  spanProcessors: [new SimpleSpanProcessor(exporter)],
263
278
  })
264
279
 
265
- const otelTracer = provider.getTracer(`testing-${strictMode ? 'strict' : 'non-strict'}`)
280
+ const otelTracer = provider.getTracer(`testing-${strictMode !== undefined ? 'strict' : 'non-strict'}`)
266
281
 
267
282
  const span = otelTracer.startSpan('test-root')
268
283
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
@@ -312,7 +327,7 @@ Vitest.describe('useClientDocument', () => {
312
327
  const stackInfo = JSON.parse(val as string) as LiveStore.StackInfo
313
328
  // stackInfo.frames.shift() // Removes `renderHook.wrapper` from the stack
314
329
  stackInfo.frames.forEach((_) => {
315
- if (_.name.includes('renderHook.wrapper')) {
330
+ if (_.name.includes('renderHook.wrapper') === true) {
316
331
  _.name = 'renderHook.wrapper'
317
332
  }
318
333
  _.filePath = '__REPLACED_FOR_SNAPSHOT__'
@@ -1,14 +1,26 @@
1
+ import React from 'react'
2
+
1
3
  import type { RowQuery } from '@livestore/common'
2
4
  import { SessionIdSymbol } from '@livestore/common'
3
5
  import { State } from '@livestore/common/schema'
6
+ import { removeUndefinedValues, type StateSetters, validateTableOptions } from '@livestore/framework-toolkit'
4
7
  import type { LiveQuery, LiveQueryDef, Store } from '@livestore/livestore'
5
8
  import { queryDb } from '@livestore/livestore'
6
9
  import { omitUndefineds, shouldNeverHappen } from '@livestore/utils'
7
- import React from 'react'
8
10
 
9
- import { LiveStoreContext } from './LiveStoreContext.ts'
10
11
  import { useQueryRef } from './useQuery.ts'
11
12
 
13
+ /**
14
+ * Return type of `useClientDocument` hook.
15
+ *
16
+ * A tuple providing React-style state access to a client-document table row:
17
+ * - `[0]` row: The current value (decoded according to the table schema)
18
+ * - `[1]` setRow: Setter function to update the document
19
+ * - `[2]` id: The document's ID (resolved from `SessionIdSymbol` if applicable)
20
+ * - `[3]` query$: The underlying `LiveQuery` for advanced use cases
21
+ *
22
+ * @typeParam TTableDef - The client-document table definition type
23
+ */
12
24
  export type UseClientDocumentResult<TTableDef extends State.SQLite.ClientDocumentTableDef.TraitAny> = [
13
25
  row: TTableDef['Value'],
14
26
  setRow: StateSetters<TTableDef>,
@@ -104,20 +116,15 @@ export const useClientDocument: {
104
116
 
105
117
  const tableName = table.sqliteDef.name
106
118
 
107
- const store =
108
- storeArg?.store ?? // biome-ignore lint/correctness/useHookAtTopLevel: store is stable
109
- React.useContext(LiveStoreContext)?.store ??
110
- shouldNeverHappen(`No store provided to useClientDocument`)
119
+ const store = storeArg?.store ?? shouldNeverHappen(`No store provided to useClientDocument`)
111
120
 
112
- // console.debug('useClientDocument', tableName, id)
113
-
114
- const idStr: string = id === SessionIdSymbol ? store.clientSession.sessionId : id
121
+ const idStr: string = id === SessionIdSymbol ? store.sessionId : id
115
122
 
116
123
  type QueryDef = LiveQueryDef<TTableDef['Value']>
117
124
  const queryDef: QueryDef = React.useMemo(
118
125
  () =>
119
- queryDb(table.get(id!, { default: defaultValues! }), {
120
- deps: [idStr!, table.sqliteDef.name, JSON.stringify(defaultValues)],
126
+ queryDb(table.get(id, { default: defaultValues! }), {
127
+ deps: [idStr, table.sqliteDef.name, JSON.stringify(defaultValues)],
121
128
  }),
122
129
  [table, id, defaultValues, idStr],
123
130
  )
@@ -132,36 +139,10 @@ export const useClientDocument: {
132
139
  const newValue = typeof newValueOrFn === 'function' ? newValueOrFn(queryRef.valueRef.current) : newValueOrFn
133
140
  if (queryRef.valueRef.current === newValue) return
134
141
 
135
- store.commit(table.set(removeUndefinedValues(newValue), id as any))
142
+ store.commit(table.set(removeUndefinedValues(newValue), id))
136
143
  },
137
144
  [id, queryRef.valueRef, store, table],
138
145
  )
139
146
 
140
147
  return [queryRef.valueRef.current, setState, idStr, queryRef.queryRcRef.value]
141
148
  }
142
-
143
- export type Dispatch<A> = (action: A) => void
144
- export type SetStateActionPartial<S> = Partial<S> | ((previousValue: S) => Partial<S>)
145
- export type SetStateAction<S> = S | ((previousValue: S) => S)
146
-
147
- export type StateSetters<TTableDef extends State.SQLite.ClientDocumentTableDef.TraitAny> = Dispatch<
148
- TTableDef[State.SQLite.ClientDocumentTableDefSymbol]['options']['partialSet'] extends false
149
- ? SetStateAction<TTableDef['Value']>
150
- : SetStateActionPartial<TTableDef['Value']>
151
- >
152
-
153
- const validateTableOptions = (table: State.SQLite.TableDef<any, any>) => {
154
- if (State.SQLite.tableIsClientDocumentTable(table) === false) {
155
- return shouldNeverHappen(
156
- `useClientDocument called on table "${table.sqliteDef.name}" which is not a client document table`,
157
- )
158
- }
159
- }
160
-
161
- const removeUndefinedValues = (value: any) => {
162
- if (typeof value === 'object' && value !== null) {
163
- return Object.fromEntries(Object.entries(value).filter(([_, v]) => v !== undefined))
164
- }
165
-
166
- return value
167
- }