on-zero 0.4.39 → 0.4.41
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.
- package/dist/cjs/createUseQuery.cjs +22 -5
- package/dist/cjs/createUseQuery.native.js +22 -5
- package/dist/cjs/createUseQuery.native.js.map +1 -1
- package/dist/cjs/createUseQueryDirect.cjs +15 -17
- package/dist/cjs/createUseQueryDirect.native.js +22 -20
- package/dist/cjs/createUseQueryDirect.native.js.map +1 -1
- package/dist/cjs/useQuery.empty.test.cjs +70 -0
- package/dist/cjs/useQuery.empty.test.native.js +87 -0
- package/dist/cjs/useQuery.empty.test.native.js.map +1 -0
- package/dist/esm/createUseQuery.mjs +22 -6
- package/dist/esm/createUseQuery.mjs.map +1 -1
- package/dist/esm/createUseQuery.native.js +22 -6
- package/dist/esm/createUseQuery.native.js.map +1 -1
- package/dist/esm/createUseQueryDirect.mjs +16 -18
- package/dist/esm/createUseQueryDirect.mjs.map +1 -1
- package/dist/esm/createUseQueryDirect.native.js +23 -21
- package/dist/esm/createUseQueryDirect.native.js.map +1 -1
- package/dist/esm/useQuery.empty.test.mjs +71 -0
- package/dist/esm/useQuery.empty.test.mjs.map +1 -0
- package/dist/esm/useQuery.empty.test.native.js +85 -0
- package/dist/esm/useQuery.empty.test.native.js.map +1 -0
- package/package.json +2 -2
- package/src/createUseQuery.tsx +41 -6
- package/src/createUseQueryDirect.tsx +27 -15
- package/src/useQuery.empty.test.tsx +98 -0
- package/types/createUseQuery.d.ts +3 -0
- package/types/createUseQuery.d.ts.map +1 -1
- package/types/createUseQueryDirect.d.ts.map +1 -1
- package/types/useQuery.empty.test.d.ts +5 -0
- package/types/useQuery.empty.test.d.ts.map +1 -0
|
@@ -8,6 +8,7 @@ import { useEmitterValue, type Emitter } from '@take-out/helpers'
|
|
|
8
8
|
import { useContext, useMemo, useRef, useSyncExternalStore, type Context } from 'react'
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
|
+
emptyResponseFor,
|
|
11
12
|
parseUseQueryArgs,
|
|
12
13
|
type QueryControlMode,
|
|
13
14
|
type UseQueryHook,
|
|
@@ -25,13 +26,10 @@ import type {
|
|
|
25
26
|
// useQuery path via createZeroClient; this exists only for nested providers
|
|
26
27
|
// where a non-innermost instance cannot be selected through react context.
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
// see createUseQuery.tsx — empty responses must match the typed contract:
|
|
30
|
+
// plural queries get [], singular get undefined. returning null breaks the
|
|
31
|
+
// obvious .filter / .find / .length on first render.
|
|
29
32
|
const DISABLED_SUBSCRIBE = () => () => {}
|
|
30
|
-
const DISABLED_SNAPSHOT: readonly [undefined, { type: 'unknown' }] = [
|
|
31
|
-
undefined,
|
|
32
|
-
{ type: 'unknown' },
|
|
33
|
-
]
|
|
34
|
-
const getDisabledSnapshot = () => DISABLED_SNAPSHOT
|
|
35
33
|
|
|
36
34
|
type DirectSnapshot = readonly [unknown, { type: string }]
|
|
37
35
|
|
|
@@ -255,16 +253,17 @@ export function createUseQueryDirect<Schema extends ZeroSchema>({
|
|
|
255
253
|
getZero,
|
|
256
254
|
zeroVersion,
|
|
257
255
|
}: Parameters<CreateUseQueryDirect<Schema>>[0]): UseQueryHook<Schema> {
|
|
258
|
-
// SSG: return an inert hook — see createUseQuery for the rationale.
|
|
256
|
+
// SSG: return an inert hook — see createUseQuery for the rationale. default
|
|
257
|
+
// to the plural empty shape (most queries are plural; a singular caller can
|
|
258
|
+
// tolerate [] better than the non-singular caller can tolerate null/undefined).
|
|
259
259
|
if (typeof window === 'undefined') {
|
|
260
|
-
return (() =>
|
|
260
|
+
return ((_fn: any) => emptyResponseFor(undefined)) as UseQueryHook<Schema>
|
|
261
261
|
}
|
|
262
262
|
|
|
263
263
|
const directViewStore = new DirectViewStore()
|
|
264
264
|
|
|
265
265
|
function useQueryDirect(...args: any[]): any {
|
|
266
266
|
const disableMode = useContext(DisabledContext)
|
|
267
|
-
const lastRef = useRef<any>(EMPTY_RESPONSE)
|
|
268
267
|
const [fn, paramsOrOptions, optionsArg] = args
|
|
269
268
|
|
|
270
269
|
const version = useEmitterValue(zeroVersion)
|
|
@@ -281,19 +280,32 @@ export function createUseQueryDirect<Schema extends ZeroSchema>({
|
|
|
281
280
|
|
|
282
281
|
const paramsKey = params === undefined ? '' : JSON.stringify(params)
|
|
283
282
|
|
|
283
|
+
// resolve the query once so we know its singular/plural format up front —
|
|
284
|
+
// the no-zero / disabled snapshot needs to match that format so .filter /
|
|
285
|
+
// .find / .length is safe on first render.
|
|
286
|
+
const queryRequest = useMemo(
|
|
287
|
+
() => resolveQuery({ customQueries, fn, params }),
|
|
288
|
+
// params is keyed by paramsKey
|
|
289
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
290
|
+
[fn, paramsKey],
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
const emptyForQuery = useMemo(() => emptyResponseFor(queryRequest), [queryRequest])
|
|
294
|
+
const lastRef = useRef<any>(emptyForQuery)
|
|
295
|
+
|
|
284
296
|
const view = useMemo((): DirectView | null => {
|
|
285
297
|
const zero = getZero()
|
|
286
298
|
if (!zero) return null
|
|
287
|
-
const queryRequest = resolveQuery({ customQueries, fn, params })
|
|
288
299
|
return directViewStore.getView(zero, queryRequest, enabled, ttl)
|
|
289
|
-
//
|
|
300
|
+
// version re-materializes on a new zero
|
|
290
301
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
291
|
-
}, [
|
|
302
|
+
}, [queryRequest, enabled, ttl, version])
|
|
292
303
|
|
|
304
|
+
const getEmpty = () => emptyForQuery as DirectSnapshot
|
|
293
305
|
const out = useSyncExternalStore(
|
|
294
306
|
view ? view.subscribe : DISABLED_SUBSCRIBE,
|
|
295
|
-
view ? view.getSnapshot :
|
|
296
|
-
view ? view.getSnapshot :
|
|
307
|
+
view ? view.getSnapshot : getEmpty,
|
|
308
|
+
view ? view.getSnapshot : getEmpty,
|
|
297
309
|
)
|
|
298
310
|
|
|
299
311
|
if (!disableMode) {
|
|
@@ -305,7 +317,7 @@ export function createUseQueryDirect<Schema extends ZeroSchema>({
|
|
|
305
317
|
return lastRef.current
|
|
306
318
|
}
|
|
307
319
|
|
|
308
|
-
return
|
|
320
|
+
return emptyForQuery
|
|
309
321
|
}
|
|
310
322
|
|
|
311
323
|
return useQueryDirect as UseQueryHook<Schema>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// useQuery's typed contract is `[T[], info]` for plural queries. The empty /
|
|
4
|
+
// loading / disabled response MUST therefore be `[[], info]` — returning
|
|
5
|
+
// `[null, info]` (the old EMPTY_RESPONSE constant) broke any caller that does
|
|
6
|
+
// the obvious .filter / .find / .length / for-of on first render. Singular
|
|
7
|
+
// queries get `[undefined, info]` instead (the established zero-react shape).
|
|
8
|
+
|
|
9
|
+
import { createSchema, number, string, table } from '@rocicorp/zero'
|
|
10
|
+
import { act } from 'react'
|
|
11
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
12
|
+
import { afterEach, beforeEach, expect, test } from 'vitest'
|
|
13
|
+
|
|
14
|
+
import { createZeroClient } from './createZeroClient'
|
|
15
|
+
import { zql } from './zql'
|
|
16
|
+
|
|
17
|
+
declare global {
|
|
18
|
+
// eslint-disable-next-line no-var
|
|
19
|
+
var IS_REACT_ACT_ENVIRONMENT: boolean | undefined
|
|
20
|
+
}
|
|
21
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true
|
|
22
|
+
|
|
23
|
+
const todoTable = table('todo')
|
|
24
|
+
.columns({ id: string(), title: string(), createdAt: number() })
|
|
25
|
+
.primaryKey('id')
|
|
26
|
+
const schema = createSchema({ tables: [todoTable] })
|
|
27
|
+
|
|
28
|
+
// real plain query functions backed by zql so resolveQuery returns a real
|
|
29
|
+
// QueryRequest (asQueryInternals(...).format.singular works).
|
|
30
|
+
const allTodos = (_args: void) =>
|
|
31
|
+
(zql as unknown as { todo: { orderBy: (k: string, d: string) => any } }).todo.orderBy(
|
|
32
|
+
'createdAt',
|
|
33
|
+
'desc',
|
|
34
|
+
)
|
|
35
|
+
const oneTodo = (args: { id: string }) =>
|
|
36
|
+
(zql as unknown as { todo: { where: (k: string, v: string) => any } }).todo
|
|
37
|
+
.where('id', args.id)
|
|
38
|
+
.one()
|
|
39
|
+
|
|
40
|
+
const client = createZeroClient({
|
|
41
|
+
schema,
|
|
42
|
+
models: {},
|
|
43
|
+
groupedQueries: {
|
|
44
|
+
todo: { allTodos, oneTodo },
|
|
45
|
+
},
|
|
46
|
+
instanceName: 'empty-shape-test',
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
let container: HTMLDivElement
|
|
50
|
+
let root: Root
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
container = document.createElement('div')
|
|
54
|
+
root = createRoot(container)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
act(() => root.unmount())
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
function renderWithDisabled<T>(useHook: () => T): T {
|
|
62
|
+
let captured: T | undefined
|
|
63
|
+
const Probe = () => {
|
|
64
|
+
captured = useHook()
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
act(() => {
|
|
68
|
+
// disable=true mounts the stable shell with DisabledContext='empty' + a
|
|
69
|
+
// stub Zero — this is exactly the path that used to return [null, ...]
|
|
70
|
+
// for every query.
|
|
71
|
+
root.render(
|
|
72
|
+
<client.ProvideZero authData={{}} userID="anon" disable>
|
|
73
|
+
<Probe />
|
|
74
|
+
</client.ProvideZero>,
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
if (captured === undefined) throw new Error('Probe did not render')
|
|
78
|
+
return captured
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// regression: previously this returned [null, ...] which crashed downstream
|
|
82
|
+
// .filter / .find / .length. now it must return [[], ...] for plural queries.
|
|
83
|
+
test('useQuery returns [] (not null) for plural queries under DisabledContext', () => {
|
|
84
|
+
const [data, info] = renderWithDisabled(() => client.useQuery(allTodos))
|
|
85
|
+
expect(Array.isArray(data)).toBe(true)
|
|
86
|
+
expect((data as unknown[]).length).toBe(0)
|
|
87
|
+
// method calls that previously crashed must not crash
|
|
88
|
+
expect((data as unknown[]).filter(() => true)).toEqual([])
|
|
89
|
+
expect((data as unknown[]).find(() => true)).toBeUndefined()
|
|
90
|
+
expect(info?.type).toBe('unknown')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('useQuery returns undefined (not null) for singular queries under DisabledContext', () => {
|
|
94
|
+
const [data, info] = renderWithDisabled(() => client.useQuery(oneTodo, { id: 'x' }))
|
|
95
|
+
// singular queries match zero-react: data is undefined while loading/disabled.
|
|
96
|
+
expect(data).toBeUndefined()
|
|
97
|
+
expect(info?.type).toBe('unknown')
|
|
98
|
+
})
|
|
@@ -14,6 +14,9 @@ export type UseQueryHook<Schema extends ZeroSchema> = {
|
|
|
14
14
|
<TArg, TTable extends keyof Schema['tables'] & string, TReturn>(fn: PlainQueryFn<TArg, Query<TTable, Schema, TReturn>>, params: TArg, options?: UseQueryOptions | boolean): QueryResult<TReturn>;
|
|
15
15
|
<TTable extends keyof Schema['tables'] & string, TReturn>(fn: PlainQueryFn<void, Query<TTable, Schema, TReturn>>, options?: UseQueryOptions | boolean): QueryResult<TReturn>;
|
|
16
16
|
};
|
|
17
|
+
export declare function emptyResponseFor(queryRequest: unknown): readonly [unknown, {
|
|
18
|
+
type: string;
|
|
19
|
+
}];
|
|
17
20
|
export declare function parseUseQueryArgs(paramsOrOptions: any, optionsArg: any): {
|
|
18
21
|
params: any;
|
|
19
22
|
options: any;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createUseQuery.d.ts","sourceRoot":"","sources":["../src/createUseQuery.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,YAAY,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"createUseQuery.d.ts","sourceRoot":"","sources":["../src/createUseQuery.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAE/D,OAAO,EAA+B,KAAK,OAAO,EAAE,MAAM,OAAO,CAAA;AAGjE,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAEhE,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EACb,KAAK,EACL,MAAM,IAAI,UAAU,EACrB,MAAM,gBAAgB,CAAA;AAGvB,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,OAAO,GAAG,YAAY,CAAA;AAE7D,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC7B,GAAG,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAA;CAC9C,CAAA;AAED,KAAK,kBAAkB,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,CAAC,CAAA;AAC5D,MAAM,MAAM,WAAW,CAAC,OAAO,IAAI,SAAS,CAAC,aAAa,CAAC,OAAO,CAAC,EAAE,kBAAkB,CAAC,CAAA;AAExF,YAAY,EAAE,YAAY,EAAE,CAAA;AAE5B,MAAM,MAAM,YAAY,CAAC,MAAM,SAAS,UAAU,IAAI;IAEpD,CAAC,IAAI,EAAE,MAAM,SAAS,MAAM,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,EAAE,OAAO,EAC5D,EAAE,EAAE,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,EACtD,MAAM,EAAE,IAAI,EACZ,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,GAClC,WAAW,CAAC,OAAO,CAAC,CAAC;IAGxB,CAAC,MAAM,SAAS,MAAM,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,EAAE,OAAO,EACtD,EAAE,EAAE,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,EACtD,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,GAClC,WAAW,CAAC,OAAO,CAAC,CAAA;CACxB,CAAA;AAiBD,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,OAAO,GAAG,SAAS,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAU5F;AAGD,wBAAgB,iBAAiB,CAAC,eAAe,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG;;;EAYtE;AAED,wBAAgB,cAAc,CAAC,MAAM,SAAS,UAAU,EAAE,EACxD,eAAe,EACf,aAAa,GACd,EAAE;IACD,eAAe,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAA;IAC1C,aAAa,EAAE,gBAAgB,CAAA;CAChC,GAAG,YAAY,CAAC,MAAM,CAAC,CAgEvB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createUseQueryDirect.d.ts","sourceRoot":"","sources":["../src/createUseQueryDirect.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAmB,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAA;AACjE,OAAO,EAAqD,KAAK,OAAO,EAAE,MAAM,OAAO,CAAA;AAEvF,OAAO,
|
|
1
|
+
{"version":3,"file":"createUseQueryDirect.d.ts","sourceRoot":"","sources":["../src/createUseQueryDirect.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAmB,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAA;AACjE,OAAO,EAAqD,KAAK,OAAO,EAAE,MAAM,OAAO,CAAA;AAEvF,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,YAAY,EAElB,MAAM,kBAAkB,CAAA;AAGzB,OAAO,KAAK,EACV,gBAAgB,EAEhB,MAAM,IAAI,UAAU,EACrB,MAAM,gBAAgB,CAAA;AAkBvB,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAChB,WAAW,CACT,KAAK,EAAE,GAAG,EACV,OAAO,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,GACtB;QACD,WAAW,CACT,EAAE,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,gBAAgB,KAAK,IAAI,GACpE,IAAI,CAAA;QACP,OAAO,IAAI,IAAI,CAAA;QACf,SAAS,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CAAA;KAC1B,CAAA;CACF,CAAA;AAED,MAAM,MAAM,oBAAoB,CAAC,MAAM,SAAS,UAAU,IAAI,CAAC,KAAK,EAAE;IACpE,eAAe,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAA;IAC1C,aAAa,EAAE,gBAAgB,CAAA;IAC/B,OAAO,EAAE,MAAM,kBAAkB,GAAG,IAAI,CAAA;IACxC,WAAW,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;CAC7B,KAAK,YAAY,CAAC,MAAM,CAAC,CAAA;AAG1B,KAAK,gBAAgB,GAAG;IACtB,KAAK,EAAE,KAAK,GAAG,OAAO,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAA;AAsLD,wBAAgB,oBAAoB,CAAC,MAAM,SAAS,UAAU,EAAE,EAC9D,eAAe,EACf,aAAa,EACb,OAAO,EACP,WAAW,GACZ,EAAE,UAAU,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,MAAM,CAAC,CAqEpE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useQuery.empty.test.d.ts","sourceRoot":"","sources":["../src/useQuery.empty.test.tsx"],"names":[],"mappings":"AAgBA,OAAO,CAAC,MAAM,CAAC;IAEb,IAAI,wBAAwB,EAAE,OAAO,GAAG,SAAS,CAAA;CAClD"}
|