@livestore/react 0.4.0-dev.1 → 0.4.0-dev.10

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.
@@ -3,6 +3,7 @@ import { provideOtel, type UnexpectedError } from '@livestore/common'
3
3
  import { Events, makeSchema, State } from '@livestore/common/schema'
4
4
  import type { LiveStoreSchema, SqliteDsl, Store } from '@livestore/livestore'
5
5
  import { createStore } from '@livestore/livestore'
6
+ import { omitUndefineds } from '@livestore/utils'
6
7
  import { Effect, Schema, type Scope } from '@livestore/utils/effect'
7
8
  import type * as otel from '@opentelemetry/api'
8
9
  import React from 'react'
@@ -60,6 +61,12 @@ const AppRouterSchema = State.SQLite.clientDocument({
60
61
  },
61
62
  })
62
63
 
64
+ const kv = State.SQLite.clientDocument({
65
+ name: 'Kv',
66
+ schema: Schema.Any,
67
+ default: { value: null },
68
+ })
69
+
63
70
  export const events = {
64
71
  todoCreated: Events.synced({
65
72
  name: 'todoCreated',
@@ -75,14 +82,15 @@ export const events = {
75
82
  }),
76
83
  AppRouterSet: AppRouterSchema.set,
77
84
  UserInfoSet: userInfo.set,
85
+ KvSet: kv.set,
78
86
  }
79
87
 
80
88
  const materializers = State.SQLite.materializers(events, {
81
89
  todoCreated: ({ id, text, completed }) => todos.insert({ id, text, completed }),
82
- todoUpdated: ({ id, text, completed }) => todos.update({ completed, text }).where({ id }),
90
+ todoUpdated: ({ id, text, completed }) => todos.update({ ...omitUndefineds({ completed, text }) }).where({ id }),
83
91
  })
84
92
 
85
- export const tables = { todos, app, userInfo, AppRouterSchema }
93
+ export const tables = { todos, app, userInfo, AppRouterSchema, kv }
86
94
 
87
95
  const state = State.SQLite.makeState({ tables, materializers })
88
96
  export const schema = makeSchema({ state, events })
@@ -92,9 +100,9 @@ export const makeTodoMvcReact: ({
92
100
  otelContext,
93
101
  strictMode,
94
102
  }?: {
95
- otelTracer?: otel.Tracer
96
- otelContext?: otel.Context
97
- strictMode?: boolean
103
+ otelTracer?: otel.Tracer | undefined
104
+ otelContext?: otel.Context | undefined
105
+ strictMode?: boolean | undefined
98
106
  }) => Effect.Effect<
99
107
  {
100
108
  wrapper: ({ children }: any) => React.JSX.Element
@@ -108,9 +116,9 @@ export const makeTodoMvcReact: ({
108
116
  otelContext,
109
117
  strictMode,
110
118
  }: {
111
- otelTracer?: otel.Tracer
112
- otelContext?: otel.Context
113
- strictMode?: boolean
119
+ otelTracer?: otel.Tracer | undefined
120
+ otelContext?: otel.Context | undefined
121
+ strictMode?: boolean | undefined
114
122
  } = {}) =>
115
123
  Effect.gen(function* () {
116
124
  const makeRenderCount = () => {
@@ -156,4 +164,4 @@ export const makeTodoMvcReact: ({
156
164
  const renderCount = makeRenderCount()
157
165
 
158
166
  return { wrapper, store: storeWithReactApi, renderCount }
159
- }).pipe(provideOtel({ parentSpanContext: otelContext, otelTracer }))
167
+ }).pipe(provideOtel(omitUndefineds({ parentSpanContext: otelContext, otelTracer })))
package/src/mod.ts CHANGED
@@ -3,8 +3,9 @@ export { LiveStoreProvider } from './LiveStoreProvider.tsx'
3
3
  export {
4
4
  type Dispatch,
5
5
  type SetStateAction,
6
+ type SetStateActionPartial,
6
7
  type StateSetters,
7
- type UseRowResult as UseStateResult,
8
+ type UseClientDocumentResult,
8
9
  useClientDocument,
9
10
  } from './useClientDocument.ts'
10
11
  export { useQuery, useQueryRef } from './useQuery.ts'
@@ -1,7 +1,7 @@
1
1
  /** biome-ignore-all lint/a11y/useValidAriaRole: not needed for testing */
2
2
  /** biome-ignore-all lint/a11y/noStaticElementInteractions: not needed for testing */
3
3
  import * as LiveStore from '@livestore/livestore'
4
- import { getSimplifiedRootSpan } from '@livestore/livestore/internal/testing-utils'
4
+ import { getAllSimplifiedRootSpans, getSimplifiedRootSpan } from '@livestore/livestore/internal/testing-utils'
5
5
  import { Effect, ReadonlyRecord, Schema } from '@livestore/utils/effect'
6
6
  import { Vitest } from '@livestore/utils-dev/node-vitest'
7
7
  import * as otel from '@opentelemetry/api'
@@ -224,6 +224,34 @@ Vitest.describe('useClientDocument', () => {
224
224
  }),
225
225
  )
226
226
 
227
+ Vitest.scopedLive('kv client document overwrites value (Schema.Any, no partial merge)', () =>
228
+ Effect.gen(function* () {
229
+ const { wrapper, store, renderCount } = yield* makeTodoMvcReact({})
230
+
231
+ const { result } = ReactTesting.renderHook(
232
+ (id: string) => {
233
+ renderCount.inc()
234
+
235
+ const [state, setState] = store.useClientDocument(tables.kv, id)
236
+ return { state, setState, id }
237
+ },
238
+ { wrapper, initialProps: 'k1' },
239
+ )
240
+
241
+ expect(result.current.id).toBe('k1')
242
+ expect(result.current.state).toBe(null)
243
+ expect(renderCount.val).toBe(1)
244
+
245
+ ReactTesting.act(() => result.current.setState(1))
246
+ expect(result.current.state).toEqual(1)
247
+ expect(renderCount.val).toBe(2)
248
+
249
+ ReactTesting.act(() => result.current.setState({ b: 2 }))
250
+ expect(result.current.state).toEqual({ b: 2 })
251
+ expect(renderCount.val).toBe(3)
252
+ }),
253
+ )
254
+
227
255
  Vitest.describe('otel', () => {
228
256
  it.each([{ strictMode: true }, { strictMode: false }])(
229
257
  'should update the data based on component key strictMode=%s',
@@ -295,7 +323,8 @@ Vitest.describe('useClientDocument', () => {
295
323
  })
296
324
  }
297
325
 
298
- expect(getSimplifiedRootSpan(exporter, mapAttributes)).toMatchSnapshot()
326
+ expect(getSimplifiedRootSpan(exporter, 'createStore', mapAttributes)).toMatchSnapshot()
327
+ expect(getAllSimplifiedRootSpans(exporter, 'LiveStore:commit', mapAttributes)).toMatchSnapshot()
299
328
 
300
329
  await provider.shutdown()
301
330
  },
@@ -3,13 +3,13 @@ import { SessionIdSymbol } from '@livestore/common'
3
3
  import { State } from '@livestore/common/schema'
4
4
  import type { LiveQuery, LiveQueryDef, Store } from '@livestore/livestore'
5
5
  import { queryDb } from '@livestore/livestore'
6
- import { shouldNeverHappen } from '@livestore/utils'
6
+ import { omitUndefineds, shouldNeverHappen } from '@livestore/utils'
7
7
  import React from 'react'
8
8
 
9
9
  import { LiveStoreContext } from './LiveStoreContext.ts'
10
10
  import { useQueryRef } from './useQuery.ts'
11
11
 
12
- export type UseRowResult<TTableDef extends State.SQLite.ClientDocumentTableDef.TraitAny> = [
12
+ export type UseClientDocumentResult<TTableDef extends State.SQLite.ClientDocumentTableDef.TraitAny> = [
13
13
  row: TTableDef['Value'],
14
14
  setRow: StateSetters<TTableDef>,
15
15
  id: string,
@@ -54,13 +54,17 @@ export const useClientDocument: {
54
54
  any,
55
55
  any,
56
56
  any,
57
- { partialSet: boolean; default: { id: string | SessionIdSymbol; value: any } }
57
+ {
58
+ partialSet: boolean
59
+ /** Default value to use instead of the default value from the table definition */
60
+ default: any
61
+ }
58
62
  >,
59
63
  >(
60
64
  table: TTableDef,
61
65
  id?: State.SQLite.ClientDocumentTableDef.DefaultIdType<TTableDef> | SessionIdSymbol,
62
66
  options?: Partial<RowQuery.GetOrCreateOptions<TTableDef>>,
63
- ): UseRowResult<TTableDef>
67
+ ): UseClientDocumentResult<TTableDef>
64
68
 
65
69
  // case: no default id → id arg is required
66
70
  <
@@ -68,20 +72,24 @@ export const useClientDocument: {
68
72
  any,
69
73
  any,
70
74
  any,
71
- { partialSet: boolean; default: { id: string | SessionIdSymbol | undefined; value: any } }
75
+ {
76
+ partialSet: boolean
77
+ /** Default value to use instead of the default value from the table definition */
78
+ default: any
79
+ }
72
80
  >,
73
81
  >(
74
82
  table: TTableDef,
75
83
  // TODO adjust so it works with arbitrary primary keys or unique constraints
76
84
  id: State.SQLite.ClientDocumentTableDef.DefaultIdType<TTableDef> | string | SessionIdSymbol,
77
85
  options?: Partial<RowQuery.GetOrCreateOptions<TTableDef>>,
78
- ): UseRowResult<TTableDef>
86
+ ): UseClientDocumentResult<TTableDef>
79
87
  } = <TTableDef extends State.SQLite.ClientDocumentTableDef.Any>(
80
88
  table: TTableDef,
81
89
  idOrOptions?: string | SessionIdSymbol,
82
90
  options_?: Partial<RowQuery.GetOrCreateOptions<TTableDef>>,
83
91
  storeArg?: { store?: Store },
84
- ): UseRowResult<TTableDef> => {
92
+ ): UseClientDocumentResult<TTableDef> => {
85
93
  const id =
86
94
  typeof idOrOptions === 'string' || idOrOptions === SessionIdSymbol
87
95
  ? idOrOptions
@@ -97,8 +105,7 @@ export const useClientDocument: {
97
105
  const tableName = table.sqliteDef.name
98
106
 
99
107
  const store =
100
- storeArg?.store ??
101
- // biome-ignore lint/correctness/useHookAtTopLevel: store is stable
108
+ storeArg?.store ?? // biome-ignore lint/correctness/useHookAtTopLevel: store is stable
102
109
  React.useContext(LiveStoreContext)?.store ??
103
110
  shouldNeverHappen(`No store provided to useClientDocument`)
104
111
 
@@ -117,7 +124,7 @@ export const useClientDocument: {
117
124
 
118
125
  const queryRef = useQueryRef(queryDef, {
119
126
  otelSpanName: `LiveStore:useClientDocument:${tableName}:${idStr}`,
120
- store: storeArg?.store,
127
+ ...omitUndefineds({ store: storeArg?.store }),
121
128
  })
122
129
 
123
130
  const setState = React.useMemo<StateSetters<TTableDef>>(
@@ -134,10 +141,13 @@ export const useClientDocument: {
134
141
  }
135
142
 
136
143
  export type Dispatch<A> = (action: A) => void
137
- export type SetStateAction<S> = Partial<S> | ((previousValue: S) => Partial<S>)
144
+ export type SetStateActionPartial<S> = Partial<S> | ((previousValue: S) => Partial<S>)
145
+ export type SetStateAction<S> = S | ((previousValue: S) => S)
138
146
 
139
147
  export type StateSetters<TTableDef extends State.SQLite.ClientDocumentTableDef.TraitAny> = Dispatch<
140
- SetStateAction<TTableDef['Value']>
148
+ TTableDef[State.SQLite.ClientDocumentTableDefSymbol]['options']['partialSet'] extends false
149
+ ? SetStateAction<TTableDef['Value']>
150
+ : SetStateActionPartial<TTableDef['Value']>
141
151
  >
142
152
 
143
153
  const validateTableOptions = (table: State.SQLite.TableDef<any, any>) => {
package/src/useQuery.ts CHANGED
@@ -42,8 +42,7 @@ export const useQueryRef = <TQuery extends LiveQueryDef.Any>(
42
42
  queryRcRef: LiveQueries.RcRef<LiveQuery<LiveQueries.GetResult<TQuery>>>
43
43
  } => {
44
44
  const store =
45
- options?.store ??
46
- // biome-ignore lint/correctness/useHookAtTopLevel: store is stable
45
+ options?.store ?? // biome-ignore lint/correctness/useHookAtTopLevel: store is stable
47
46
  React.useContext(LiveStoreContext)?.store ??
48
47
  shouldNeverHappen(`No store provided to useQuery`)
49
48