@livestore/react 0.4.0-dev.20 → 0.4.0-dev.22
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/.tsbuildinfo +1 -1
- package/dist/StoreRegistryContext.d.ts +56 -0
- package/dist/StoreRegistryContext.d.ts.map +1 -0
- package/dist/StoreRegistryContext.js +61 -0
- package/dist/StoreRegistryContext.js.map +1 -0
- package/dist/__tests__/fixture.d.ts.map +1 -1
- package/dist/__tests__/fixture.js +1 -6
- package/dist/__tests__/fixture.js.map +1 -1
- package/dist/experimental/components/LiveList.d.ts +4 -2
- package/dist/experimental/components/LiveList.d.ts.map +1 -1
- package/dist/experimental/components/LiveList.js +6 -5
- package/dist/experimental/components/LiveList.js.map +1 -1
- package/dist/experimental/mod.d.ts +0 -1
- package/dist/experimental/mod.d.ts.map +1 -1
- package/dist/experimental/mod.js +0 -1
- package/dist/experimental/mod.js.map +1 -1
- package/dist/mod.d.ts +4 -3
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +3 -2
- package/dist/mod.js.map +1 -1
- package/dist/useClientDocument.d.ts +33 -0
- package/dist/useClientDocument.d.ts.map +1 -1
- package/dist/useClientDocument.js +1 -4
- package/dist/useClientDocument.js.map +1 -1
- package/dist/useQuery.d.ts +1 -1
- package/dist/useQuery.d.ts.map +1 -1
- package/dist/useQuery.js +2 -5
- package/dist/useQuery.js.map +1 -1
- package/dist/useStore.d.ts +62 -7
- package/dist/useStore.d.ts.map +1 -1
- package/dist/useStore.js +73 -15
- package/dist/useStore.js.map +1 -1
- package/dist/useStore.test.d.ts.map +1 -0
- package/dist/useStore.test.js +196 -0
- package/dist/useStore.test.js.map +1 -0
- package/package.json +7 -7
- package/src/StoreRegistryContext.tsx +69 -0
- package/src/__tests__/fixture.tsx +1 -13
- package/src/experimental/components/LiveList.tsx +13 -4
- package/src/experimental/mod.ts +0 -1
- package/src/mod.ts +4 -3
- package/src/useClientDocument.ts +36 -5
- package/src/useQuery.ts +2 -6
- package/src/useStore.test.tsx +271 -0
- package/src/useStore.ts +102 -23
- package/dist/LiveStoreContext.d.ts +0 -13
- package/dist/LiveStoreContext.d.ts.map +0 -1
- package/dist/LiveStoreContext.js +0 -3
- package/dist/LiveStoreContext.js.map +0 -1
- package/dist/LiveStoreProvider.d.ts +0 -66
- package/dist/LiveStoreProvider.d.ts.map +0 -1
- package/dist/LiveStoreProvider.js +0 -232
- package/dist/LiveStoreProvider.js.map +0 -1
- package/dist/LiveStoreProvider.test.d.ts +0 -2
- package/dist/LiveStoreProvider.test.d.ts.map +0 -1
- package/dist/LiveStoreProvider.test.js +0 -117
- package/dist/LiveStoreProvider.test.js.map +0 -1
- package/dist/experimental/multi-store/StoreRegistry.d.ts +0 -61
- package/dist/experimental/multi-store/StoreRegistry.d.ts.map +0 -1
- package/dist/experimental/multi-store/StoreRegistry.js +0 -275
- package/dist/experimental/multi-store/StoreRegistry.js.map +0 -1
- package/dist/experimental/multi-store/StoreRegistry.test.d.ts +0 -2
- package/dist/experimental/multi-store/StoreRegistry.test.d.ts.map +0 -1
- package/dist/experimental/multi-store/StoreRegistry.test.js +0 -464
- package/dist/experimental/multi-store/StoreRegistry.test.js.map +0 -1
- package/dist/experimental/multi-store/StoreRegistryContext.d.ts +0 -10
- package/dist/experimental/multi-store/StoreRegistryContext.d.ts.map +0 -1
- package/dist/experimental/multi-store/StoreRegistryContext.js +0 -15
- package/dist/experimental/multi-store/StoreRegistryContext.js.map +0 -1
- package/dist/experimental/multi-store/mod.d.ts +0 -6
- package/dist/experimental/multi-store/mod.d.ts.map +0 -1
- package/dist/experimental/multi-store/mod.js +0 -6
- package/dist/experimental/multi-store/mod.js.map +0 -1
- package/dist/experimental/multi-store/storeOptions.d.ts +0 -4
- package/dist/experimental/multi-store/storeOptions.d.ts.map +0 -1
- package/dist/experimental/multi-store/storeOptions.js +0 -4
- package/dist/experimental/multi-store/storeOptions.js.map +0 -1
- package/dist/experimental/multi-store/types.d.ts +0 -44
- package/dist/experimental/multi-store/types.d.ts.map +0 -1
- package/dist/experimental/multi-store/types.js +0 -2
- package/dist/experimental/multi-store/types.js.map +0 -1
- package/dist/experimental/multi-store/useStore.d.ts +0 -11
- package/dist/experimental/multi-store/useStore.d.ts.map +0 -1
- package/dist/experimental/multi-store/useStore.js +0 -21
- package/dist/experimental/multi-store/useStore.js.map +0 -1
- package/dist/experimental/multi-store/useStore.test.d.ts.map +0 -1
- package/dist/experimental/multi-store/useStore.test.js +0 -144
- package/dist/experimental/multi-store/useStore.test.js.map +0 -1
- package/src/LiveStoreContext.ts +0 -14
- package/src/LiveStoreProvider.test.tsx +0 -248
- package/src/LiveStoreProvider.tsx +0 -421
- package/src/experimental/multi-store/StoreRegistry.test.ts +0 -631
- package/src/experimental/multi-store/StoreRegistry.ts +0 -347
- package/src/experimental/multi-store/StoreRegistryContext.tsx +0 -23
- package/src/experimental/multi-store/mod.ts +0 -5
- package/src/experimental/multi-store/storeOptions.ts +0 -8
- package/src/experimental/multi-store/types.ts +0 -55
- package/src/experimental/multi-store/useStore.test.tsx +0 -197
- package/src/experimental/multi-store/useStore.ts +0 -34
- /package/dist/{experimental/multi-store/useStore.test.d.ts → useStore.test.d.ts} +0 -0
package/dist/useStore.js
CHANGED
|
@@ -1,7 +1,79 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { useStoreRegistry } from "./StoreRegistryContext.js";
|
|
3
3
|
import { useClientDocument } from "./useClientDocument.js";
|
|
4
4
|
import { useQuery } from "./useQuery.js";
|
|
5
|
+
/**
|
|
6
|
+
* Returns a store instance augmented with hooks (`store.useQuery()` and `store.useClientDocument()`) for reactive queries.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* function Issue() {
|
|
11
|
+
* // Suspends until loaded or returns immediately if already loaded
|
|
12
|
+
* const issueStore = useStore(issueStoreOptions('abc123'))
|
|
13
|
+
* const [issue] = issueStore.useQuery(queryDb(tables.issue.select()))
|
|
14
|
+
*
|
|
15
|
+
* const toggleStatus = () =>
|
|
16
|
+
* issueStore.commit(
|
|
17
|
+
* issueEvents.issueStatusChanged({
|
|
18
|
+
* id: issue.id,
|
|
19
|
+
* status: issue.status === 'done' ? 'todo' : 'done',
|
|
20
|
+
* }),
|
|
21
|
+
* )
|
|
22
|
+
*
|
|
23
|
+
* const preloadParentIssue = (issueId: string) =>
|
|
24
|
+
* storeRegistry.preload({
|
|
25
|
+
* ...issueStoreOptions(issueId),
|
|
26
|
+
* unusedCacheTime: 10_000,
|
|
27
|
+
* })
|
|
28
|
+
*
|
|
29
|
+
* return (
|
|
30
|
+
* <>
|
|
31
|
+
* <h2>{issue.title}</h2>
|
|
32
|
+
* <button onClick={() => toggleStatus()}>Toggle Status</button>
|
|
33
|
+
* <button onMouseEnter={() => preloadParentIssue(issue.parentIssueId)}>Open Parent Issue</button>
|
|
34
|
+
* </>
|
|
35
|
+
* )
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @remarks
|
|
40
|
+
* - Suspends until the store is loaded.
|
|
41
|
+
* - Store is cached by its `storeId` in the `StoreRegistry`. Multiple calls with the same `storeId` return the same store instance.
|
|
42
|
+
* - Store is cached as long as it's being used, and after `unusedCacheTime` expires (default `60_000` ms in browser, `Infinity` in non-browser)
|
|
43
|
+
* - Default store options can be configured in `StoreRegistry` constructor.
|
|
44
|
+
* - Store options are only applied when the store is loaded. Subsequent calls with different options will not affect the store if it's already loaded and cached in the registry.
|
|
45
|
+
*
|
|
46
|
+
* @typeParam TSchema - The schema type for the store
|
|
47
|
+
* @returns The loaded store instance augmented with React hooks
|
|
48
|
+
* @throws unknown - store loading error or if called outside `<StoreRegistryProvider>`
|
|
49
|
+
*/
|
|
50
|
+
export const useStore = (options) => {
|
|
51
|
+
const storeRegistry = useStoreRegistry();
|
|
52
|
+
// NOTE: retain() is called in useEffect (after render), while getOrLoadPromise() is called
|
|
53
|
+
// in useMemo (during render). This creates a timing gap where with very short unusedCacheTime
|
|
54
|
+
// values (e.g., 0), the store could theoretically be disposed before the effect fires.
|
|
55
|
+
// In practice, this is not an issue with the default 60s cache time, but it becomes an issue when
|
|
56
|
+
// `unusedCacheTime` is configured to values less than ~100ms.
|
|
57
|
+
// See https://github.com/livestorejs/livestore/issues/916
|
|
58
|
+
React.useEffect(() => storeRegistry.retain(options), [storeRegistry, options]);
|
|
59
|
+
const storeOrPromise = React.useMemo(() => storeRegistry.getOrLoadPromise(options), [storeRegistry, options]);
|
|
60
|
+
const store = storeOrPromise instanceof Promise ? React.use(storeOrPromise) : storeOrPromise;
|
|
61
|
+
// Expose store on the global object for browser console debugging.
|
|
62
|
+
globalThis.__debugLiveStore ??= {};
|
|
63
|
+
if (Object.keys(globalThis.__debugLiveStore).length === 0) {
|
|
64
|
+
globalThis.__debugLiveStore._ = store;
|
|
65
|
+
}
|
|
66
|
+
globalThis.__debugLiveStore[options.debug?.instanceId ?? options.storeId] = store;
|
|
67
|
+
return withReactApi(store);
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Augments a Store instance with React-specific methods (`useQuery`, `useClientDocument`).
|
|
71
|
+
*
|
|
72
|
+
* This is called automatically by `useStore()`. You typically don't need to call it
|
|
73
|
+
* directly unless you're building custom integrations.
|
|
74
|
+
*
|
|
75
|
+
* @internal
|
|
76
|
+
*/
|
|
5
77
|
export const withReactApi = (store) => {
|
|
6
78
|
// @ts-expect-error TODO properly implement this
|
|
7
79
|
store.useQuery = (queryable) => useQuery(queryable, { store });
|
|
@@ -9,18 +81,4 @@ export const withReactApi = (store) => {
|
|
|
9
81
|
store.useClientDocument = (table, idOrOptions, options) => useClientDocument(table, idOrOptions, options, { store });
|
|
10
82
|
return store;
|
|
11
83
|
};
|
|
12
|
-
export const useStore = (options) => {
|
|
13
|
-
if (options?.store !== undefined) {
|
|
14
|
-
return { store: withReactApi(options.store) };
|
|
15
|
-
}
|
|
16
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: store is stable
|
|
17
|
-
const storeContext = React.useContext(LiveStoreContext);
|
|
18
|
-
if (storeContext === undefined) {
|
|
19
|
-
throw new Error(`useStore can only be used inside StoreContext.Provider`);
|
|
20
|
-
}
|
|
21
|
-
if (storeContext.stage !== 'running') {
|
|
22
|
-
throw new Error(`useStore can only be used after the store is running`);
|
|
23
|
-
}
|
|
24
|
-
return { store: withReactApi(storeContext.store) };
|
|
25
|
-
};
|
|
26
84
|
//# sourceMappingURL=useStore.js.map
|
package/dist/useStore.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useStore.js","sourceRoot":"","sources":["../src/useStore.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useStore.js","sourceRoot":"","sources":["../src/useStore.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA4B,CAAA;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAExC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAKtB,OAAoE,EAC/B,EAAE;IACvC,MAAM,aAAa,GAAG,gBAAgB,EAAE,CAAA;IAExC,2FAA2F;IAC3F,8FAA8F;IAC9F,uFAAuF;IACvF,kGAAkG;IAClG,8DAA8D;IAC9D,0DAA0D;IAC1D,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC,CAAA;IAE9E,MAAM,cAAc,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,gBAAgB,CAAC,OAAO,CAAC,EAAE,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC,CAAA;IAE7G,MAAM,KAAK,GAAG,cAAc,YAAY,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,cAAc,CAAA;IAE5F,mEAAmE;IACnE,UAAU,CAAC,gBAAgB,KAAK,EAAE,CAAA;IAClC,IAAI,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1D,UAAU,CAAC,gBAAgB,CAAC,CAAC,GAAG,KAAK,CAAA;IACvC,CAAC;IACD,UAAU,CAAC,gBAAgB,CAAC,OAAO,CAAC,KAAK,EAAE,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC,GAAG,KAAK,CAAA;IAEjF,OAAO,YAAY,CAAC,KAAK,CAAC,CAAA;AAC5B,CAAC,CAAA;AAgBD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAC1B,KAA+B,EACM,EAAE;IACvC,gDAAgD;IAChD,KAAK,CAAC,QAAQ,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC,QAAQ,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,CAAC,CAAA;IAE9D,gDAAgD;IAChD,KAAK,CAAC,iBAAiB,GAAG,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,EAAE,CAAC,iBAAiB,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,CAAA;IACpH,OAAO,KAA4C,CAAA;AACrD,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useStore.test.d.ts","sourceRoot":"","sources":["../src/useStore.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { makeInMemoryAdapter } from '@livestore/adapter-web';
|
|
3
|
+
import { StoreInternalsSymbol, StoreRegistry, storeOptions, } from '@livestore/livestore';
|
|
4
|
+
import { shouldNeverHappen } from '@livestore/utils';
|
|
5
|
+
import { act, render, renderHook, waitFor } from '@testing-library/react';
|
|
6
|
+
import * as React from 'react';
|
|
7
|
+
import { describe, expect, it } from 'vitest';
|
|
8
|
+
import { schema } from "./__tests__/fixture.js";
|
|
9
|
+
import { StoreRegistryProvider } from "./StoreRegistryContext.js";
|
|
10
|
+
import { useStore } from "./useStore.js";
|
|
11
|
+
describe('experimental useStore', () => {
|
|
12
|
+
it('should return the same promise instance for concurrent getOrLoadStore calls', async () => {
|
|
13
|
+
const storeRegistry = new StoreRegistry();
|
|
14
|
+
const options = testStoreOptions();
|
|
15
|
+
// Make two concurrent calls during loading
|
|
16
|
+
const firstStore = storeRegistry.getOrLoadPromise(options);
|
|
17
|
+
const secondStore = storeRegistry.getOrLoadPromise(options);
|
|
18
|
+
// Both should be promises (store is loading)
|
|
19
|
+
expect(firstStore).toBeInstanceOf(Promise);
|
|
20
|
+
expect(secondStore).toBeInstanceOf(Promise);
|
|
21
|
+
// EXPECTED BEHAVIOR: Same promise instance for React.use() compatibility
|
|
22
|
+
// ACTUAL BEHAVIOR: Different promise instances (Effect.runPromise creates new wrapper)
|
|
23
|
+
expect(firstStore).toBe(secondStore);
|
|
24
|
+
// Cleanup
|
|
25
|
+
await firstStore;
|
|
26
|
+
await cleanupAfterUnmount(() => { });
|
|
27
|
+
});
|
|
28
|
+
it('works with Suspense boundary', async () => {
|
|
29
|
+
const storeRegistry = new StoreRegistry();
|
|
30
|
+
const options = testStoreOptions();
|
|
31
|
+
let view;
|
|
32
|
+
await act(async () => {
|
|
33
|
+
view = render(_jsx(StoreRegistryProvider, { storeRegistry: storeRegistry, children: _jsx(React.Suspense, { fallback: _jsx("div", { "data-testid": "fallback" }), children: _jsx(StoreConsumer, { options: options }) }) }));
|
|
34
|
+
});
|
|
35
|
+
const renderedView = view ?? shouldNeverHappen('render failed');
|
|
36
|
+
// After loading completes, should show the actual content
|
|
37
|
+
await waitForSuspenseResolved(renderedView);
|
|
38
|
+
expect(renderedView.getByTestId('ready')).toBeDefined();
|
|
39
|
+
await cleanupAfterUnmount(() => renderedView.unmount());
|
|
40
|
+
});
|
|
41
|
+
it('does not re-suspend on subsequent renders when store is already loaded', async () => {
|
|
42
|
+
const storeRegistry = new StoreRegistry();
|
|
43
|
+
const options = testStoreOptions();
|
|
44
|
+
const Wrapper = ({ opts }) => (_jsx(StoreRegistryProvider, { storeRegistry: storeRegistry, children: _jsx(React.Suspense, { fallback: _jsx("div", { "data-testid": "fallback" }), children: _jsx(StoreConsumer, { options: opts }) }) }));
|
|
45
|
+
let view;
|
|
46
|
+
await act(async () => {
|
|
47
|
+
view = render(_jsx(Wrapper, { opts: options }));
|
|
48
|
+
});
|
|
49
|
+
const renderedView = view ?? shouldNeverHappen('render failed');
|
|
50
|
+
// Wait for initial load
|
|
51
|
+
await waitForSuspenseResolved(renderedView);
|
|
52
|
+
expect(renderedView.getByTestId('ready')).toBeDefined();
|
|
53
|
+
// Rerender with new options object (but same storeId)
|
|
54
|
+
await act(async () => {
|
|
55
|
+
renderedView.rerender(_jsx(Wrapper, { opts: { ...options } }));
|
|
56
|
+
});
|
|
57
|
+
// Should not show fallback
|
|
58
|
+
expect(renderedView.queryByTestId('fallback')).toBeNull();
|
|
59
|
+
expect(renderedView.getByTestId('ready')).toBeDefined();
|
|
60
|
+
await cleanupAfterUnmount(() => renderedView.unmount());
|
|
61
|
+
});
|
|
62
|
+
it('throws when store loading fails', async () => {
|
|
63
|
+
const storeRegistry = new StoreRegistry();
|
|
64
|
+
const badOptions = testStoreOptions({
|
|
65
|
+
// @ts-expect-error - intentionally passing invalid adapter to trigger error
|
|
66
|
+
adapter: null,
|
|
67
|
+
});
|
|
68
|
+
// Pre-load the store to cache the error (error happens synchronously)
|
|
69
|
+
expect(() => storeRegistry.getOrLoadPromise(badOptions)).toThrow();
|
|
70
|
+
// Now when useStore tries to get it, it should throw synchronously
|
|
71
|
+
expect(() => renderHook(() => useStore(badOptions), {
|
|
72
|
+
wrapper: makeProvider(storeRegistry),
|
|
73
|
+
})).toThrow();
|
|
74
|
+
});
|
|
75
|
+
it.each([
|
|
76
|
+
{ label: 'non-strict mode', strictMode: false },
|
|
77
|
+
{ label: 'strict mode', strictMode: true },
|
|
78
|
+
])('works in $label', async ({ strictMode }) => {
|
|
79
|
+
const storeRegistry = new StoreRegistry();
|
|
80
|
+
const options = testStoreOptions();
|
|
81
|
+
let hook;
|
|
82
|
+
await act(async () => {
|
|
83
|
+
hook = renderHook(() => useStore(options), {
|
|
84
|
+
wrapper: makeProvider(storeRegistry, { suspense: true }),
|
|
85
|
+
reactStrictMode: strictMode,
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
const { result, unmount } = hook ?? shouldNeverHappen('renderHook failed');
|
|
89
|
+
// Wait for store to be ready
|
|
90
|
+
await waitForStoreReady(result);
|
|
91
|
+
expect(result.current[StoreInternalsSymbol].clientSession).toBeDefined();
|
|
92
|
+
await cleanupAfterUnmount(unmount);
|
|
93
|
+
});
|
|
94
|
+
it('handles switching between different storeId values', async () => {
|
|
95
|
+
const storeRegistry = new StoreRegistry();
|
|
96
|
+
const optionsA = testStoreOptions({ storeId: 'store-a' });
|
|
97
|
+
const optionsB = testStoreOptions({ storeId: 'store-b' });
|
|
98
|
+
let hook;
|
|
99
|
+
await act(async () => {
|
|
100
|
+
hook = renderHook((opts) => useStore(opts), {
|
|
101
|
+
initialProps: optionsA,
|
|
102
|
+
wrapper: makeProvider(storeRegistry, { suspense: true }),
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
const { result, rerender, unmount } = hook ?? shouldNeverHappen('renderHook failed');
|
|
106
|
+
// Wait for first store to load
|
|
107
|
+
await waitForStoreReady(result);
|
|
108
|
+
const storeA = result.current;
|
|
109
|
+
expect(storeA[StoreInternalsSymbol].clientSession).toBeDefined();
|
|
110
|
+
// Switch to different storeId
|
|
111
|
+
await act(async () => {
|
|
112
|
+
rerender(optionsB);
|
|
113
|
+
});
|
|
114
|
+
// Wait for second store to load and verify it's different from the first
|
|
115
|
+
await waitFor(() => {
|
|
116
|
+
expect(result.current).not.toBe(storeA);
|
|
117
|
+
expect(result.current?.[StoreInternalsSymbol].clientSession).toBeDefined();
|
|
118
|
+
});
|
|
119
|
+
const storeB = result.current;
|
|
120
|
+
expect(storeB[StoreInternalsSymbol].clientSession).toBeDefined();
|
|
121
|
+
expect(storeB).not.toBe(storeA);
|
|
122
|
+
await cleanupAfterUnmount(unmount);
|
|
123
|
+
});
|
|
124
|
+
// useStore doesn't handle unusedCacheTime=0 correctly because retain is called in useEffect (after render)
|
|
125
|
+
// See https://github.com/livestorejs/livestore/issues/916
|
|
126
|
+
it.skip('should load store with unusedCacheTime set to 0', async () => {
|
|
127
|
+
const storeRegistry = new StoreRegistry({ defaultOptions: { unusedCacheTime: 0 } });
|
|
128
|
+
const options = testStoreOptions({ unusedCacheTime: 0 });
|
|
129
|
+
const StoreConsumerWithVerification = ({ opts }) => {
|
|
130
|
+
const store = useStore(opts);
|
|
131
|
+
// Verify store is usable - access internals to confirm it's not disposed
|
|
132
|
+
const clientSession = store[StoreInternalsSymbol].clientSession;
|
|
133
|
+
return _jsx("div", { "data-testid": "ready", "data-has-session": String(clientSession !== undefined) });
|
|
134
|
+
};
|
|
135
|
+
let view;
|
|
136
|
+
await act(async () => {
|
|
137
|
+
view = render(_jsx(StoreRegistryProvider, { storeRegistry: storeRegistry, children: _jsx(React.Suspense, { fallback: _jsx("div", { "data-testid": "fallback" }), children: _jsx(StoreConsumerWithVerification, { opts: options }) }) }));
|
|
138
|
+
});
|
|
139
|
+
const renderedView = view ?? shouldNeverHappen('render failed');
|
|
140
|
+
await waitForSuspenseResolved(renderedView);
|
|
141
|
+
// Store should be usable while component is mounted
|
|
142
|
+
const readyElement = renderedView.getByTestId('ready');
|
|
143
|
+
expect(readyElement.getAttribute('data-has-session')).toBe('true');
|
|
144
|
+
// Allow some time to pass to ensure store isn't prematurely disposed
|
|
145
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
146
|
+
// Store should still be usable after waiting
|
|
147
|
+
expect(readyElement.getAttribute('data-has-session')).toBe('true');
|
|
148
|
+
await cleanupAfterUnmount(() => renderedView.unmount());
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
const StoreConsumer = ({ options }) => {
|
|
152
|
+
useStore(options);
|
|
153
|
+
return _jsx("div", { "data-testid": "ready" });
|
|
154
|
+
};
|
|
155
|
+
const makeProvider = (storeRegistry, { suspense = false } = {}) => ({ children }) => {
|
|
156
|
+
let content = _jsx(StoreRegistryProvider, { storeRegistry: storeRegistry, children: children });
|
|
157
|
+
if (suspense) {
|
|
158
|
+
content = _jsx(React.Suspense, { fallback: null, children: content });
|
|
159
|
+
}
|
|
160
|
+
return content;
|
|
161
|
+
};
|
|
162
|
+
let testStoreCounter = 0;
|
|
163
|
+
const testStoreOptions = (overrides = {}) => storeOptions({
|
|
164
|
+
storeId: overrides.storeId ?? `test-store-${testStoreCounter++}`,
|
|
165
|
+
schema,
|
|
166
|
+
adapter: makeInMemoryAdapter(),
|
|
167
|
+
...overrides,
|
|
168
|
+
});
|
|
169
|
+
/**
|
|
170
|
+
* Cleans up after component unmount and waits for pending operations to settle.
|
|
171
|
+
*
|
|
172
|
+
* When components using stores unmount, the StoreRegistry schedules garbage collection
|
|
173
|
+
* timers for inactive stores. This helper waits for those timers to complete naturally.
|
|
174
|
+
*/
|
|
175
|
+
const cleanupAfterUnmount = async (cleanup) => {
|
|
176
|
+
cleanup();
|
|
177
|
+
// Allow any pending microtasks/timers to settle
|
|
178
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* Waits for React Suspense fallback to resolve and the actual content to render.
|
|
182
|
+
*/
|
|
183
|
+
const waitForSuspenseResolved = async (view) => {
|
|
184
|
+
await waitFor(() => expect(view.queryByTestId('fallback')).toBeNull());
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Waits for a store to be fully loaded and ready to use.
|
|
188
|
+
* The store is considered ready when it has a defined clientSession.
|
|
189
|
+
*/
|
|
190
|
+
const waitForStoreReady = async (result) => {
|
|
191
|
+
await waitFor(() => {
|
|
192
|
+
expect(result.current).not.toBeNull();
|
|
193
|
+
expect(result.current[StoreInternalsSymbol].clientSession).toBeDefined();
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
//# sourceMappingURL=useStore.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useStore.test.js","sourceRoot":"","sources":["../src/useStore.test.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livestore/react",
|
|
3
|
-
"version": "0.4.0-dev.
|
|
3
|
+
"version": "0.4.0-dev.22",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"exports": {
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@opentelemetry/api": "1.9.0",
|
|
12
|
-
"@livestore/common": "0.4.0-dev.
|
|
13
|
-
"@livestore/livestore": "0.4.0-dev.
|
|
14
|
-
"@livestore/utils": "0.4.0-dev.
|
|
12
|
+
"@livestore/common": "0.4.0-dev.22",
|
|
13
|
+
"@livestore/livestore": "0.4.0-dev.22",
|
|
14
|
+
"@livestore/utils": "0.4.0-dev.22"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@opentelemetry/sdk-trace-base": "2.0
|
|
17
|
+
"@opentelemetry/sdk-trace-base": "2.2.0",
|
|
18
18
|
"@testing-library/dom": "^10.4.1",
|
|
19
19
|
"@testing-library/react": "^16.3.0",
|
|
20
20
|
"@types/react": "19.1.13",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"typescript": "5.9.2",
|
|
27
27
|
"vite": "7.2.4",
|
|
28
28
|
"vitest": "3.2.4",
|
|
29
|
-
"@livestore/utils-dev": "0.4.0-dev.
|
|
30
|
-
"@livestore/adapter-web": "0.4.0-dev.
|
|
29
|
+
"@livestore/utils-dev": "0.4.0-dev.22",
|
|
30
|
+
"@livestore/adapter-web": "0.4.0-dev.22"
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
33
|
"package.json",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { StoreRegistry } from '@livestore/livestore'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
|
|
4
|
+
export const StoreRegistryContext = React.createContext<StoreRegistry | undefined>(undefined)
|
|
5
|
+
|
|
6
|
+
export type StoreRegistryProviderProps = {
|
|
7
|
+
storeRegistry: StoreRegistry
|
|
8
|
+
children: React.ReactNode
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* React context provider that makes a {@link StoreRegistry} available to descendant components.
|
|
13
|
+
*
|
|
14
|
+
* Wrap your application (or a subtree) with this provider to enable {@link useStore} and
|
|
15
|
+
* {@link useStoreRegistry} hooks within that tree.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* import { StoreRegistry } from '@livestore/livestore'
|
|
20
|
+
* import { StoreRegistryProvider } from '@livestore/react'
|
|
21
|
+
* import { unstable_batchedUpdates as batchUpdates } from 'react-dom'
|
|
22
|
+
*
|
|
23
|
+
* const storeRegistry = new StoreRegistry({
|
|
24
|
+
* defaultOptions: { batchUpdates }
|
|
25
|
+
* })
|
|
26
|
+
*
|
|
27
|
+
* function App() {
|
|
28
|
+
* return (
|
|
29
|
+
* <StoreRegistryProvider storeRegistry={storeRegistry}>
|
|
30
|
+
* <MyComponent />
|
|
31
|
+
* </StoreRegistryProvider>
|
|
32
|
+
* )
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export const StoreRegistryProvider = ({ storeRegistry, children }: StoreRegistryProviderProps): React.JSX.Element => {
|
|
37
|
+
return <StoreRegistryContext value={storeRegistry}>{children}</StoreRegistryContext>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Hook to access the {@link StoreRegistry} from context. Useful for advanced operations like preloading.
|
|
42
|
+
*
|
|
43
|
+
* @param override - Optional registry to use instead of the context value.
|
|
44
|
+
* When provided, skips context lookup entirely.
|
|
45
|
+
* @returns The registry provided by the nearest {@link StoreRegistryProvider} ancestor, or the `override` if provided.
|
|
46
|
+
* @throws Error if called outside a {@link StoreRegistryProvider} and no override is provided
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```tsx
|
|
50
|
+
* function PreloadButton({ issueId }: { issueId: string }) {
|
|
51
|
+
* const storeRegistry = useStoreRegistry()
|
|
52
|
+
*
|
|
53
|
+
* const handleMouseEnter = () => {
|
|
54
|
+
* storeRegistry.preload(issueStoreOptions(issueId))
|
|
55
|
+
* }
|
|
56
|
+
*
|
|
57
|
+
* return <button onMouseEnter={handleMouseEnter}>View Issue</button>
|
|
58
|
+
* }
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export const useStoreRegistry = (override?: StoreRegistry) => {
|
|
62
|
+
if (override) return override
|
|
63
|
+
|
|
64
|
+
const storeRegistry = React.use(StoreRegistryContext)
|
|
65
|
+
|
|
66
|
+
if (!storeRegistry) throw new Error('useStoreRegistry() must be used within <StoreRegistryProvider>')
|
|
67
|
+
|
|
68
|
+
return storeRegistry
|
|
69
|
+
}
|
|
@@ -136,21 +136,9 @@ export const makeTodoMvcReact: (opts?: MakeTodoMvcReactOptions) => Effect.Effect
|
|
|
136
136
|
|
|
137
137
|
const storeWithReactApi = LiveStoreReact.withReactApi(store)
|
|
138
138
|
|
|
139
|
-
// TODO improve typing of `LiveStoreContext`
|
|
140
|
-
const storeContext = {
|
|
141
|
-
stage: 'running' as const,
|
|
142
|
-
store: storeWithReactApi,
|
|
143
|
-
}
|
|
144
|
-
|
|
145
139
|
const MaybeStrictMode = strictMode ? React.StrictMode : React.Fragment
|
|
146
140
|
|
|
147
|
-
const wrapper = ({ children }: any) =>
|
|
148
|
-
<MaybeStrictMode>
|
|
149
|
-
<LiveStoreReact.LiveStoreContext.Provider value={storeContext}>
|
|
150
|
-
{children}
|
|
151
|
-
</LiveStoreReact.LiveStoreContext.Provider>
|
|
152
|
-
</MaybeStrictMode>
|
|
153
|
-
)
|
|
141
|
+
const wrapper = ({ children }: any) => <MaybeStrictMode>{children}</MaybeStrictMode>
|
|
154
142
|
|
|
155
143
|
const renderCount = makeRenderCount()
|
|
156
144
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LiveQueryDef } from '@livestore/livestore'
|
|
1
|
+
import type { LiveQueryDef, Store } from '@livestore/livestore'
|
|
2
2
|
import { computed } from '@livestore/livestore'
|
|
3
3
|
import React from 'react'
|
|
4
4
|
|
|
@@ -16,6 +16,8 @@ export type LiveListProps<TItem> = {
|
|
|
16
16
|
renderItem: (item: TItem, opts: { index: number; isInitialListRender: boolean }) => React.ReactNode
|
|
17
17
|
/** Needs to be unique across all list items */
|
|
18
18
|
getKey: (item: TItem, index: number) => string | number
|
|
19
|
+
/** The store instance to use for queries */
|
|
20
|
+
store: Store<any, any>
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/**
|
|
@@ -26,12 +28,15 @@ export type LiveListProps<TItem> = {
|
|
|
26
28
|
* In the future we want to make this component even more efficient by using incremental rendering (https://github.com/livestorejs/livestore/pull/55)
|
|
27
29
|
* e.g. when an item is added/removed/moved to only re-render the affected DOM nodes.
|
|
28
30
|
*/
|
|
29
|
-
export const LiveList = <TItem,>({ items$, renderItem, getKey }: LiveListProps<TItem>): React.ReactNode => {
|
|
31
|
+
export const LiveList = <TItem,>({ items$, renderItem, getKey, store }: LiveListProps<TItem>): React.ReactNode => {
|
|
30
32
|
const [hasMounted, setHasMounted] = React.useState(false)
|
|
31
33
|
|
|
32
34
|
React.useEffect(() => setHasMounted(true), [])
|
|
33
35
|
|
|
34
|
-
const keys = useQuery(
|
|
36
|
+
const keys = useQuery(
|
|
37
|
+
computed((get) => get(items$).map(getKey)),
|
|
38
|
+
{ store },
|
|
39
|
+
)
|
|
35
40
|
const arr = React.useMemo(
|
|
36
41
|
() =>
|
|
37
42
|
keys.map(
|
|
@@ -54,6 +59,7 @@ export const LiveList = <TItem,>({ items$, renderItem, getKey }: LiveListProps<T
|
|
|
54
59
|
key={key}
|
|
55
60
|
itemKey={key}
|
|
56
61
|
item$={item$}
|
|
62
|
+
store={store}
|
|
57
63
|
opts={{ isInitialListRender: !hasMounted, index }}
|
|
58
64
|
renderItem={renderItem}
|
|
59
65
|
/>
|
|
@@ -66,13 +72,15 @@ const ItemWrapper = <TItem,>({
|
|
|
66
72
|
item$,
|
|
67
73
|
opts,
|
|
68
74
|
renderItem,
|
|
75
|
+
store,
|
|
69
76
|
}: {
|
|
70
77
|
itemKey: string | number
|
|
71
78
|
item$: LiveQueryDef<TItem>
|
|
72
79
|
opts: { index: number; isInitialListRender: boolean }
|
|
73
80
|
renderItem: (item: TItem, opts: { index: number; isInitialListRender: boolean }) => React.ReactNode
|
|
81
|
+
store: Store<any, any>
|
|
74
82
|
}) => {
|
|
75
|
-
const item = useQuery(item
|
|
83
|
+
const item = useQuery(item$, { store })
|
|
76
84
|
|
|
77
85
|
return <>{renderItem(item, opts)}</>
|
|
78
86
|
}
|
|
@@ -82,6 +90,7 @@ const ItemWrapperMemo = React.memo(
|
|
|
82
90
|
(prev, next) =>
|
|
83
91
|
prev.itemKey === next.itemKey &&
|
|
84
92
|
prev.renderItem === next.renderItem &&
|
|
93
|
+
prev.store === next.store &&
|
|
85
94
|
prev.opts.index === next.opts.index &&
|
|
86
95
|
prev.opts.isInitialListRender === next.opts.isInitialListRender,
|
|
87
96
|
) as typeof ItemWrapper
|
package/src/experimental/mod.ts
CHANGED
package/src/mod.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
1
|
+
export { StoreRegistry, storeOptions } from '@livestore/livestore'
|
|
2
|
+
export { LiveList, type LiveListProps } from './experimental/components/LiveList.tsx'
|
|
3
|
+
export * from './StoreRegistryContext.tsx'
|
|
3
4
|
export {
|
|
4
5
|
type Dispatch,
|
|
5
6
|
type SetStateAction,
|
|
@@ -9,5 +10,5 @@ export {
|
|
|
9
10
|
useClientDocument,
|
|
10
11
|
} from './useClientDocument.ts'
|
|
11
12
|
export { useQuery, useQueryRef } from './useQuery.ts'
|
|
12
|
-
export { useStore, withReactApi } from './useStore.ts'
|
|
13
|
+
export { type ReactApi, useStore, withReactApi } from './useStore.ts'
|
|
13
14
|
export { useStackInfo } from './utils/stack-info.ts'
|
package/src/useClientDocument.ts
CHANGED
|
@@ -6,9 +6,19 @@ import { queryDb } from '@livestore/livestore'
|
|
|
6
6
|
import { omitUndefineds, shouldNeverHappen } from '@livestore/utils'
|
|
7
7
|
import React from 'react'
|
|
8
8
|
|
|
9
|
-
import { LiveStoreContext } from './LiveStoreContext.ts'
|
|
10
9
|
import { useQueryRef } from './useQuery.ts'
|
|
11
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Return type of `useClientDocument` hook.
|
|
13
|
+
*
|
|
14
|
+
* A tuple providing React-style state access to a client-document table row:
|
|
15
|
+
* - `[0]` row: The current value (decoded according to the table schema)
|
|
16
|
+
* - `[1]` setRow: Setter function to update the document
|
|
17
|
+
* - `[2]` id: The document's ID (resolved from `SessionIdSymbol` if applicable)
|
|
18
|
+
* - `[3]` query$: The underlying `LiveQuery` for advanced use cases
|
|
19
|
+
*
|
|
20
|
+
* @typeParam TTableDef - The client-document table definition type
|
|
21
|
+
*/
|
|
12
22
|
export type UseClientDocumentResult<TTableDef extends State.SQLite.ClientDocumentTableDef.TraitAny> = [
|
|
13
23
|
row: TTableDef['Value'],
|
|
14
24
|
setRow: StateSetters<TTableDef>,
|
|
@@ -104,10 +114,7 @@ export const useClientDocument: {
|
|
|
104
114
|
|
|
105
115
|
const tableName = table.sqliteDef.name
|
|
106
116
|
|
|
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`)
|
|
117
|
+
const store = storeArg?.store ?? shouldNeverHappen(`No store provided to useClientDocument`)
|
|
111
118
|
|
|
112
119
|
// console.debug('useClientDocument', tableName, id)
|
|
113
120
|
|
|
@@ -140,10 +147,34 @@ export const useClientDocument: {
|
|
|
140
147
|
return [queryRef.valueRef.current, setState, idStr, queryRef.queryRcRef.value]
|
|
141
148
|
}
|
|
142
149
|
|
|
150
|
+
/**
|
|
151
|
+
* A function that dispatches an action. Mirrors React's `Dispatch` type.
|
|
152
|
+
* @typeParam A - The action type
|
|
153
|
+
*/
|
|
143
154
|
export type Dispatch<A> = (action: A) => void
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* A state update that can be either a partial value or a function returning a partial value.
|
|
158
|
+
* Used when the client-document table has `partialSet: true`.
|
|
159
|
+
* @typeParam S - The state type
|
|
160
|
+
*/
|
|
144
161
|
export type SetStateActionPartial<S> = Partial<S> | ((previousValue: S) => Partial<S>)
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* A state update that can be either a full value or a function returning a full value.
|
|
165
|
+
* Mirrors React's `SetStateAction` type.
|
|
166
|
+
* @typeParam S - The state type
|
|
167
|
+
*/
|
|
145
168
|
export type SetStateAction<S> = S | ((previousValue: S) => S)
|
|
146
169
|
|
|
170
|
+
/**
|
|
171
|
+
* The setter function type for `useClientDocument`, determined by the table's `partialSet` option.
|
|
172
|
+
*
|
|
173
|
+
* - If `partialSet: false` (default), requires full state replacement
|
|
174
|
+
* - If `partialSet: true`, accepts partial updates merged with existing state
|
|
175
|
+
*
|
|
176
|
+
* @typeParam TTableDef - The client-document table definition type
|
|
177
|
+
*/
|
|
147
178
|
export type StateSetters<TTableDef extends State.SQLite.ClientDocumentTableDef.TraitAny> = Dispatch<
|
|
148
179
|
TTableDef[State.SQLite.ClientDocumentTableDefSymbol]['options']['partialSet'] extends false
|
|
149
180
|
? SetStateAction<TTableDef['Value']>
|
package/src/useQuery.ts
CHANGED
|
@@ -14,7 +14,6 @@ import { deepEqual, indent, shouldNeverHappen } from '@livestore/utils'
|
|
|
14
14
|
import * as otel from '@opentelemetry/api'
|
|
15
15
|
import React from 'react'
|
|
16
16
|
|
|
17
|
-
import { LiveStoreContext } from './LiveStoreContext.ts'
|
|
18
17
|
import { useRcResource } from './useRcResource.ts'
|
|
19
18
|
import { originalStackLimit } from './utils/stack-info.ts'
|
|
20
19
|
import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInput.ts'
|
|
@@ -49,7 +48,7 @@ export const useQuery = <TQueryable extends Queryable<any>>(
|
|
|
49
48
|
*
|
|
50
49
|
* Parameters
|
|
51
50
|
* - `queryable`: The query definition/instance/builder to run and subscribe to.
|
|
52
|
-
* - `options.store`:
|
|
51
|
+
* - `options.store`: The store to use. Required when calling `useQueryRef` directly; automatically provided when using `store.useQuery()`.
|
|
53
52
|
* - `options.otelContext`: Optional parent otel context for the query span.
|
|
54
53
|
* - `options.otelSpanName`: Optional explicit span name; otherwise derived from the query label.
|
|
55
54
|
*
|
|
@@ -71,10 +70,7 @@ export const useQueryRef = <TQueryable extends Queryable<any>>(
|
|
|
71
70
|
valueRef: React.RefObject<Queryable.Result<TQueryable>>
|
|
72
71
|
queryRcRef: LiveQueries.RcRef<LiveQuery<Queryable.Result<TQueryable>>>
|
|
73
72
|
} => {
|
|
74
|
-
const store =
|
|
75
|
-
options?.store ?? // biome-ignore lint/correctness/useHookAtTopLevel: store is stable
|
|
76
|
-
React.useContext(LiveStoreContext)?.store ??
|
|
77
|
-
shouldNeverHappen(`No store provided to useQuery`)
|
|
73
|
+
const store = options?.store ?? shouldNeverHappen(`No store provided to useQuery`)
|
|
78
74
|
|
|
79
75
|
type TResult = Queryable.Result<TQueryable>
|
|
80
76
|
type NormalizedQueryable =
|