@hvakr/firestate 0.1.3 → 0.1.4
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/README.md +91 -22
- package/dist/index.d.mts +194 -39
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +173 -45
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -220,22 +220,33 @@ function App() {
|
|
|
220
220
|
|
|
221
221
|
```tsx
|
|
222
222
|
// ProjectEditor.tsx
|
|
223
|
-
import {
|
|
223
|
+
import {
|
|
224
|
+
useDocument,
|
|
225
|
+
useCollection,
|
|
226
|
+
useDocumentSyncStatus,
|
|
227
|
+
useUndoManager,
|
|
228
|
+
} from '@hvakr/firestate'
|
|
224
229
|
import { projectDoc, spacesCollection } from './schemas'
|
|
225
230
|
|
|
226
231
|
function ProjectEditor({ projectId }: { projectId: string }) {
|
|
227
232
|
const params = { projectId }
|
|
228
233
|
|
|
229
|
-
// Subscribe to project document
|
|
234
|
+
// Subscribe to project document. The default handle is sync-agnostic — it
|
|
235
|
+
// carries `data`/`isLoaded`/`error`, not `isSynced`, so it does NOT
|
|
236
|
+
// re-render when a save settles.
|
|
230
237
|
const project = useDocument({ definition: projectDoc, params })
|
|
231
238
|
|
|
232
239
|
// Subscribe to spaces collection (lazy)
|
|
233
240
|
const spaces = useCollection({ definition: spacesCollection, params })
|
|
234
241
|
|
|
242
|
+
// Opt into save state only where you render it — shares the project's one
|
|
243
|
+
// listener, so it doesn't add a subscription.
|
|
244
|
+
const { isSaving } = useDocumentSyncStatus({ definition: projectDoc, params })
|
|
245
|
+
|
|
235
246
|
// Access undo/redo
|
|
236
247
|
const { undo, redo, canUndo, canRedo } = useUndoManager()
|
|
237
248
|
|
|
238
|
-
if (project.
|
|
249
|
+
if (!project.isLoaded) return <Spinner />
|
|
239
250
|
if (!project.data) return <NotFound />
|
|
240
251
|
|
|
241
252
|
return (
|
|
@@ -253,7 +264,7 @@ function ProjectEditor({ projectId }: { projectId: string }) {
|
|
|
253
264
|
{/* Lazy-load spaces */}
|
|
254
265
|
{!spaces.isActive ? (
|
|
255
266
|
<button onClick={spaces.load}>Load Spaces</button>
|
|
256
|
-
) : spaces.
|
|
267
|
+
) : !spaces.isLoaded ? (
|
|
257
268
|
<Spinner />
|
|
258
269
|
) : (
|
|
259
270
|
<ul>
|
|
@@ -266,7 +277,7 @@ function ProjectEditor({ projectId }: { projectId: string }) {
|
|
|
266
277
|
)}
|
|
267
278
|
|
|
268
279
|
{/* Sync indicator */}
|
|
269
|
-
{
|
|
280
|
+
{isSaving && <span>Saving...</span>}
|
|
270
281
|
</div>
|
|
271
282
|
)
|
|
272
283
|
}
|
|
@@ -622,8 +633,7 @@ const {
|
|
|
622
633
|
update, // Update with partial diff
|
|
623
634
|
set, // Replace entire document
|
|
624
635
|
delete: del, // Delete the document
|
|
625
|
-
|
|
626
|
-
isSynced, // Whether all changes are synced
|
|
636
|
+
isLoaded, // Whether the initial snapshot has arrived (ready to render)
|
|
627
637
|
sync, // Force sync immediately
|
|
628
638
|
error, // Error from listener, if any
|
|
629
639
|
ref, // Firestore DocumentReference
|
|
@@ -634,6 +644,12 @@ const {
|
|
|
634
644
|
undoable: true, // Optional: enable undo (default: true)
|
|
635
645
|
enabled: true, // Optional: set false until required params exist
|
|
636
646
|
})
|
|
647
|
+
|
|
648
|
+
// The default handle is SYNC-AGNOSTIC: no `isSynced`, so a save settling does
|
|
649
|
+
// not re-render it. For save state, use the per-entry sync-status hook (with the
|
|
650
|
+
// registry API) or fold `isSynced` into a `selector`. `isLoading` likewise moved
|
|
651
|
+
// to the loading-status hook; the data handle keeps `isLoaded` for the common
|
|
652
|
+
// "spinner until ready" gate.
|
|
637
653
|
```
|
|
638
654
|
|
|
639
655
|
#### `useCollection(options)`
|
|
@@ -646,9 +662,8 @@ const {
|
|
|
646
662
|
update, // Update one or more documents
|
|
647
663
|
add, // Add a new document (explicit or auto-generated id)
|
|
648
664
|
remove, // Remove a document
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
isActive, // Whether subscription is active
|
|
665
|
+
isLoaded, // Active AND past the initial load (isActive && !isLoading)
|
|
666
|
+
isActive, // Whether subscription is active (for lazy collections)
|
|
652
667
|
load, // Activate a lazy subscription
|
|
653
668
|
sync, // Force sync immediately
|
|
654
669
|
error, // Error from listener, if any
|
|
@@ -688,12 +703,14 @@ remove('oldSpaceId')
|
|
|
688
703
|
|
|
689
704
|
#### Selecting a slice (`selector` + `isEqual`)
|
|
690
705
|
|
|
691
|
-
By default a component re-renders
|
|
692
|
-
the subscribed document
|
|
693
|
-
handle** (`data`, `
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
706
|
+
By default a component re-renders when the data, the load state (`isLoaded`), or
|
|
707
|
+
`error` of the subscribed document/collection changes, and the hook returns the
|
|
708
|
+
**sync-agnostic default handle** (`data`, `isLoaded`, `error`, the writers, and
|
|
709
|
+
`ref` — plus a collection's `isActive`). It deliberately omits `isSynced`, so a
|
|
710
|
+
save settling does not re-render it (see [Sync status and loading status](#sync-status-and-loading-status)).
|
|
711
|
+
Pass a `selector` to take further control: it receives the resource's *full*
|
|
712
|
+
observable state — including `isLoading`/`isSynced` — and returns the slice the
|
|
713
|
+
component reacts to, so the component re-renders **only** when that slice changes.
|
|
697
714
|
|
|
698
715
|
A selected handle exposes exactly your slice as `data`, plus the writer surface
|
|
699
716
|
(`update`/`set`/`delete`/`add`/`remove`/`load`/`sync`) and `ref` — the status
|
|
@@ -764,6 +781,59 @@ writable hook on the same resource, so the common provider/leaf pattern (one
|
|
|
764
781
|
writable owner, many `readOnly: true` read-selectors) sees the writer's
|
|
765
782
|
optimistic edits live. Only the read-only handle's own writers are disabled.
|
|
766
783
|
|
|
784
|
+
#### Sync status and loading status
|
|
785
|
+
|
|
786
|
+
The default data handle is **sync-agnostic**: it carries `data`/`isLoaded`/
|
|
787
|
+
`error` but never `isSynced`. That matters because `isSynced` flips on *every*
|
|
788
|
+
autosave settle — so if the data handle carried it, every component that merely
|
|
789
|
+
reads a record would re-render an extra time after each save. Most readers only
|
|
790
|
+
want the data; the few that render save state (a "Saving…" indicator, a
|
|
791
|
+
navigation blocker) opt in explicitly.
|
|
792
|
+
|
|
793
|
+
For each registry entry, `createFirestate` generates two opt-in status hooks
|
|
794
|
+
beside the data hook:
|
|
795
|
+
|
|
796
|
+
```typescript
|
|
797
|
+
const { useSpaces, useSpacesSyncStatus, useSpacesLoadingStatus } =
|
|
798
|
+
createFirestate({ spaces: spacesEntry })
|
|
799
|
+
|
|
800
|
+
// Only this component re-renders when a save settles — not every data reader.
|
|
801
|
+
function SaveIndicator(params) {
|
|
802
|
+
const { isSynced, isSaving } = useSpacesSyncStatus(params)
|
|
803
|
+
return isSaving ? <Spinner /> : <Check />
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// A spinner that shows load progress WITHOUT re-rendering when data changes.
|
|
807
|
+
function SpacesSpinner(params) {
|
|
808
|
+
const { isLoading, isLoaded } = useSpacesLoadingStatus(params)
|
|
809
|
+
return isLoading ? <Spinner /> : null
|
|
810
|
+
}
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
Both share the entry's **one** `onSnapshot` listener with the data hook (and any
|
|
814
|
+
slice hooks) — sharing is keyed by `(definition, path, query)`, not by which
|
|
815
|
+
hook you call — so opting in costs no extra subscription. `useSpacesSyncStatus`
|
|
816
|
+
re-renders only when sync state flips; `useSpacesLoadingStatus` re-renders only
|
|
817
|
+
on the load transition, never on data. Collection status hooks take the same
|
|
818
|
+
`queryConstraints` as the data hook (pass the same query to share the listener).
|
|
819
|
+
|
|
820
|
+
On a **lazy** collection, a status hook does not call `load()` itself — so as
|
|
821
|
+
the *only* subscriber it stays idle (`{ isSynced: true, isSaving: false }` /
|
|
822
|
+
`{ isLoading: false, isLoaded: false }`) and attaches no listener. Pair it with
|
|
823
|
+
the data hook, whose `load()` activates the one shared listener the status hook
|
|
824
|
+
then rides. Non-lazy collections activate on mount, so this is lazy-only.
|
|
825
|
+
|
|
826
|
+
`.select` (slice) entries do **not** get their own status hooks — a slice's sync
|
|
827
|
+
and loading state is the resource's, read through the base entry's status hooks.
|
|
828
|
+
|
|
829
|
+
With the lower-level API there are standalone equivalents —
|
|
830
|
+
`useDocumentSyncStatus` / `useDocumentLoadingStatus` /
|
|
831
|
+
`useCollectionSyncStatus` / `useCollectionLoadingStatus`, each taking
|
|
832
|
+
`{ definition, params, enabled }` (collections also `queryConstraints`).
|
|
833
|
+
|
|
834
|
+
This is the per-resource counterpart to [`useIsSynced()`](#useissynced), which
|
|
835
|
+
reports a single provider-wide aggregate across *all* tracked resources.
|
|
836
|
+
|
|
767
837
|
#### `useUndoManager()`
|
|
768
838
|
|
|
769
839
|
Access the undo manager.
|
|
@@ -915,7 +985,7 @@ const combined = mergeDiffs(diff1, diff2)
|
|
|
915
985
|
## Notes
|
|
916
986
|
|
|
917
987
|
- **`enabled` flag** — pass `enabled: false` to generated hooks or to `useDocument`/`useCollection` when route params or auth-derived ids are not ready yet. Disabled hooks do not resolve paths or attach listeners, which avoids building invalid Firestore paths like `projects//spaces`.
|
|
918
|
-
- **Navigation flicker** — changing `params` rebuilds the listener and briefly shows `
|
|
988
|
+
- **Navigation flicker** — changing `params` rebuilds the listener and briefly shows the loading state (`isLoaded: false`). To keep the previous data visible across the transition, wrap your param in `useDeferredValue`.
|
|
919
989
|
- **No cross-doc transactions** — writes are atomic per document and per collection (via `writeBatch`), but not across them. For now, use Firestore's `runTransaction` directly via `handle.ref`.
|
|
920
990
|
- **Per-client undo** — `useUndoManager` is local; one user's undo doesn't propagate to others.
|
|
921
991
|
- **Multi-tab sync** — handled automatically by Firestore's listeners; no extra setup.
|
|
@@ -1008,9 +1078,9 @@ const project = useDocument({
|
|
|
1008
1078
|
params: { projectId: '123' },
|
|
1009
1079
|
})
|
|
1010
1080
|
|
|
1011
|
-
// Missing documents are not errors — `data` is undefined
|
|
1012
|
-
//
|
|
1013
|
-
if (
|
|
1081
|
+
// Missing documents are not errors — once loaded, `data` is undefined.
|
|
1082
|
+
// Render a create/empty state for that case.
|
|
1083
|
+
if (project.isLoaded && !project.data) {
|
|
1014
1084
|
return <CreateProject />
|
|
1015
1085
|
}
|
|
1016
1086
|
|
|
@@ -1075,8 +1145,7 @@ vi.mock('@hvakr/firestate', () => ({
|
|
|
1075
1145
|
update: vi.fn(),
|
|
1076
1146
|
set: vi.fn(),
|
|
1077
1147
|
delete: vi.fn(),
|
|
1078
|
-
|
|
1079
|
-
isSynced: true,
|
|
1148
|
+
isLoaded: true,
|
|
1080
1149
|
sync: vi.fn(),
|
|
1081
1150
|
error: undefined,
|
|
1082
1151
|
ref: {},
|
package/dist/index.d.mts
CHANGED
|
@@ -27,27 +27,43 @@ interface UpdateOptions {
|
|
|
27
27
|
undoGroupId?: string;
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
30
|
+
* The full observable state of a document subscription — what a hook `selector`
|
|
31
|
+
* receives. Carries every status flag (including `isSynced`, which the default
|
|
32
|
+
* data handle deliberately omits) so a selector can react to exactly the slice
|
|
33
|
+
* it reads.
|
|
31
34
|
*/
|
|
32
35
|
interface DocumentState<T> {
|
|
33
36
|
/** Current merged state (local changes applied to sync state) */
|
|
34
37
|
data: T | undefined;
|
|
35
|
-
/** Whether initial
|
|
38
|
+
/** Whether the initial snapshot has not arrived yet */
|
|
36
39
|
isLoading: boolean;
|
|
37
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Whether the initial snapshot has arrived and data is ready to render — the
|
|
42
|
+
* completion of {@link DocumentState.isLoading} (`!isLoading` for a live
|
|
43
|
+
* subscription; `false` while the hook is disabled).
|
|
44
|
+
*/
|
|
45
|
+
isLoaded: boolean;
|
|
46
|
+
/** Whether all local changes have synced to Firestore (no pending writes) */
|
|
38
47
|
isSynced: boolean;
|
|
39
48
|
/** Error from listener, if any */
|
|
40
49
|
error: Error | undefined;
|
|
41
50
|
}
|
|
42
51
|
/**
|
|
43
|
-
*
|
|
52
|
+
* The full observable state of a collection subscription — what a hook
|
|
53
|
+
* `selector` receives. See {@link DocumentState}.
|
|
44
54
|
*/
|
|
45
55
|
interface CollectionState<T> {
|
|
46
56
|
/** Current merged state keyed by document ID */
|
|
47
57
|
data: Record<string, T>;
|
|
48
|
-
/** Whether initial
|
|
58
|
+
/** Whether the initial snapshot has not arrived yet */
|
|
49
59
|
isLoading: boolean;
|
|
50
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Whether the collection is active and its initial snapshot has arrived —
|
|
62
|
+
* `isActive && !isLoading`. `false` for a lazy collection before `load()`,
|
|
63
|
+
* and while the hook is disabled.
|
|
64
|
+
*/
|
|
65
|
+
isLoaded: boolean;
|
|
66
|
+
/** Whether all local changes have synced to Firestore (no pending writes) */
|
|
51
67
|
isSynced: boolean;
|
|
52
68
|
/** Whether the collection has been activated (for lazy loading) */
|
|
53
69
|
isActive: boolean;
|
|
@@ -55,7 +71,38 @@ interface CollectionState<T> {
|
|
|
55
71
|
error: Error | undefined;
|
|
56
72
|
}
|
|
57
73
|
/**
|
|
58
|
-
*
|
|
74
|
+
* Sync status of a single resource, returned by the per-entry
|
|
75
|
+
* `use{Name}SyncStatus` hook. Opt-in: only components that render save/dirty
|
|
76
|
+
* state subscribe to it, so the common data path does not re-render when a write
|
|
77
|
+
* settles. Shares the resource's one `onSnapshot` listener.
|
|
78
|
+
*/
|
|
79
|
+
interface SyncStatus {
|
|
80
|
+
/** Whether all local changes have synced to Firestore (no pending writes) */
|
|
81
|
+
isSynced: boolean;
|
|
82
|
+
/** Whether there are pending local changes still being saved (`!isSynced`) */
|
|
83
|
+
isSaving: boolean;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Loading status of a single resource, returned by the per-entry
|
|
87
|
+
* `use{Name}LoadingStatus` hook. A spinner-only channel: it re-renders on load
|
|
88
|
+
* transitions but never on data changes. Shares the resource's listener.
|
|
89
|
+
*/
|
|
90
|
+
interface LoadingStatus {
|
|
91
|
+
/** Whether the initial snapshot has not arrived yet */
|
|
92
|
+
isLoading: boolean;
|
|
93
|
+
/** Whether the initial snapshot has arrived (the completion of `isLoading`) */
|
|
94
|
+
isLoaded: boolean;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Document handle returned by the `useDocument` hook.
|
|
98
|
+
*
|
|
99
|
+
* **Sync-agnostic by default.** The handle carries `data`, `isLoaded`, `error`,
|
|
100
|
+
* the writers, and `ref` — but NOT `isSynced`. A document hook therefore does
|
|
101
|
+
* not re-render when a write settles (the `isSynced` flip on every autosave),
|
|
102
|
+
* so "just render the record" is the cheap, default path. Components that
|
|
103
|
+
* actually render save/dirty state opt into the per-entry `use{Name}SyncStatus`
|
|
104
|
+
* hook ({@link SyncStatus}), which shares the same listener. The raw
|
|
105
|
+
* `isLoading`/`isSynced` flags remain on {@link DocumentState} for selectors.
|
|
59
106
|
*/
|
|
60
107
|
interface DocumentHandle<T extends FirestoreObject> {
|
|
61
108
|
/** Current document data */
|
|
@@ -66,10 +113,13 @@ interface DocumentHandle<T extends FirestoreObject> {
|
|
|
66
113
|
set: (data: T, options?: UpdateOptions) => void;
|
|
67
114
|
/** Delete the document */
|
|
68
115
|
delete: (options?: UpdateOptions) => void;
|
|
69
|
-
/**
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Whether the initial snapshot has arrived and data is ready to render — the
|
|
118
|
+
* completion of `isLoading`. `false` while loading or when the hook is
|
|
119
|
+
* disabled. (Use `use{Name}LoadingStatus` for an `isLoading`/`isLoaded`
|
|
120
|
+
* channel that does not re-render on data changes.)
|
|
121
|
+
*/
|
|
122
|
+
isLoaded: boolean;
|
|
73
123
|
/** Force sync pending changes immediately */
|
|
74
124
|
sync: () => Promise<void>;
|
|
75
125
|
/** Error from listener, if any */
|
|
@@ -81,7 +131,13 @@ interface DocumentHandle<T extends FirestoreObject> {
|
|
|
81
131
|
ref: DocumentReference<T> | undefined;
|
|
82
132
|
}
|
|
83
133
|
/**
|
|
84
|
-
* Collection handle returned by useCollection hook
|
|
134
|
+
* Collection handle returned by the `useCollection` hook.
|
|
135
|
+
*
|
|
136
|
+
* Sync-agnostic by default, exactly like {@link DocumentHandle}: it carries
|
|
137
|
+
* `data`, `isLoaded`, `isActive`, `error`, the writers, `load`, and `ref` — but
|
|
138
|
+
* NOT `isSynced`. Opt into `use{Name}SyncStatus` for save state. `isActive`
|
|
139
|
+
* stays (lazy collections gate a "Load" button on it); `isLoaded` is
|
|
140
|
+
* `isActive && !isLoading`.
|
|
85
141
|
*/
|
|
86
142
|
interface CollectionHandle<T extends FirestoreObject> {
|
|
87
143
|
/** Current collection data keyed by document ID */
|
|
@@ -102,10 +158,12 @@ interface CollectionHandle<T extends FirestoreObject> {
|
|
|
102
158
|
};
|
|
103
159
|
/** Remove a document from the collection */
|
|
104
160
|
remove: (id: string, options?: UpdateOptions) => void;
|
|
105
|
-
/**
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
161
|
+
/**
|
|
162
|
+
* Whether the collection is active and its initial snapshot has arrived
|
|
163
|
+
* (ready to render) — `isActive && !isLoading`. `false` for a lazy
|
|
164
|
+
* collection before `load()`, while loading, or when the hook is disabled.
|
|
165
|
+
*/
|
|
166
|
+
isLoaded: boolean;
|
|
109
167
|
/** Whether subscription is active (for lazy collections) */
|
|
110
168
|
isActive: boolean;
|
|
111
169
|
/** Activate a lazy subscription */
|
|
@@ -121,7 +179,7 @@ interface CollectionHandle<T extends FirestoreObject> {
|
|
|
121
179
|
ref: CollectionReference<T> | undefined;
|
|
122
180
|
}
|
|
123
181
|
/** Reactive status fields a selector drops unless it folds them into its slice. */
|
|
124
|
-
type DocumentStatusKeys = "
|
|
182
|
+
type DocumentStatusKeys = "isLoaded" | "error";
|
|
125
183
|
type CollectionStatusKeys = DocumentStatusKeys | "isActive";
|
|
126
184
|
/**
|
|
127
185
|
* A {@link DocumentHandle} reduced to a hook-level `selector`'s output. The
|
|
@@ -131,8 +189,8 @@ type CollectionStatusKeys = DocumentStatusKeys | "isActive";
|
|
|
131
189
|
*
|
|
132
190
|
* A selected handle deliberately exposes **only** `data` (the slice) plus the
|
|
133
191
|
* writer surface (`update`/`set`/`delete`/`sync`) and `ref` — never the status
|
|
134
|
-
* fields (`
|
|
135
|
-
*
|
|
192
|
+
* fields (`isLoaded`/`error`). Status is not a freebie here: if a component
|
|
193
|
+
* needs it, it must select it (`s => ({ slice: s.data?.x, loading:
|
|
136
194
|
* s.isLoading })`), so what you re-render on is exactly what you select. The
|
|
137
195
|
* writers stay typed against the full document `TData`, because a selector
|
|
138
196
|
* changes what you *read*, never what you *write*.
|
|
@@ -152,9 +210,9 @@ interface SelectedDocumentHandle<TData extends FirestoreObject, TSelected> exten
|
|
|
152
210
|
* with {@link SelectedDocumentHandle}, the selector receives the full
|
|
153
211
|
* observable state ({@link CollectionState}) and the handle exposes only the
|
|
154
212
|
* slice plus the writer surface (`update`/`add`/`remove`/`load`/`sync`) and
|
|
155
|
-
* `ref` — status fields (`
|
|
156
|
-
*
|
|
157
|
-
*
|
|
213
|
+
* `ref` — status fields (`isLoaded`/`isActive`/`error`) are dropped unless
|
|
214
|
+
* folded into the slice. Writers stay typed against the full collection of
|
|
215
|
+
* `TData`.
|
|
158
216
|
*/
|
|
159
217
|
interface SelectedCollectionHandle<TData extends FirestoreObject, TSelected> extends Omit<CollectionHandle<TData>, "data" | CollectionStatusKeys> {
|
|
160
218
|
/** The slice produced by the hook's `selector`. */
|
|
@@ -506,8 +564,9 @@ interface DocumentSelectorOptions<TData extends FirestoreObject, TSelected> {
|
|
|
506
564
|
/**
|
|
507
565
|
* Project the document's observable state down to the slice this component
|
|
508
566
|
* reacts to. The selector receives the full {@link DocumentState} —
|
|
509
|
-
* `{ data, isLoading, isSynced, error }`, where `data` is
|
|
510
|
-
* the document is loading or the hook is disabled — and
|
|
567
|
+
* `{ data, isLoading, isLoaded, isSynced, error }`, where `data` is
|
|
568
|
+
* `undefined` while the document is loading or the hook is disabled — and
|
|
569
|
+
* the component
|
|
511
570
|
* re-renders *only* when the returned slice changes (per `isEqual`). Status
|
|
512
571
|
* is not a freebie: read `s.isLoading`/`s.isSynced`/`s.error` here if you
|
|
513
572
|
* want to react to them (e.g. `s => ({ title: s.data?.title, saving:
|
|
@@ -533,8 +592,9 @@ interface CollectionSelectorOptions<TData extends FirestoreObject, TSelected> {
|
|
|
533
592
|
/**
|
|
534
593
|
* Project the collection's observable state down to the slice this component
|
|
535
594
|
* reacts to. The selector receives the full {@link CollectionState} —
|
|
536
|
-
* `{ data, isLoading, isSynced, isActive, error }`, where `data` is
|
|
537
|
-
* record (e.g. `s => s.data[id]` or `s => Object.keys(s.data)`) —
|
|
595
|
+
* `{ data, isLoading, isLoaded, isSynced, isActive, error }`, where `data` is
|
|
596
|
+
* the keyed record (e.g. `s => s.data[id]` or `s => Object.keys(s.data)`) —
|
|
597
|
+
* and the
|
|
538
598
|
* component re-renders *only* when the returned slice changes. As with
|
|
539
599
|
* {@link DocumentSelectorOptions.selector}, status is reactive only if you
|
|
540
600
|
* select it, and the returned handle exposes just the slice plus
|
|
@@ -583,9 +643,8 @@ interface UseDocumentOptions<TData extends FirestoreObject> {
|
|
|
583
643
|
undoable?: boolean;
|
|
584
644
|
/**
|
|
585
645
|
* If false, no subscription is created and a no-op handle is returned
|
|
586
|
-
* (`{ data: undefined,
|
|
587
|
-
*
|
|
588
|
-
* Default: true.
|
|
646
|
+
* (`{ data: undefined, isLoaded: false, ref: undefined }`). Use this to gate
|
|
647
|
+
* subscriptions on route params that aren't ready yet. Default: true.
|
|
589
648
|
*/
|
|
590
649
|
enabled?: boolean;
|
|
591
650
|
}
|
|
@@ -607,10 +666,15 @@ interface UseDocumentOptions<TData extends FirestoreObject> {
|
|
|
607
666
|
* route params aren't ready yet).
|
|
608
667
|
*
|
|
609
668
|
* **SSR.** On the server there is no Firestore listener, so this hook returns
|
|
610
|
-
* the initial handle (`{ data: undefined,
|
|
669
|
+
* the initial handle (`{ data: undefined, isLoaded: false }`). Mutations like
|
|
611
670
|
* `update`/`set` will mutate orphaned local state with no effect — avoid
|
|
612
671
|
* calling them server-side.
|
|
613
672
|
*
|
|
673
|
+
* The default handle is **sync-agnostic** — it carries `data`/`isLoaded`/`error`
|
|
674
|
+
* but not `isSynced`, so it does not re-render when a write settles. Render save
|
|
675
|
+
* state via the per-entry `use{Name}SyncStatus` hook, or fold `isSynced` into a
|
|
676
|
+
* `selector`.
|
|
677
|
+
*
|
|
614
678
|
* @example
|
|
615
679
|
* ```tsx
|
|
616
680
|
* const projectDoc = defineDocument<Project>({
|
|
@@ -619,12 +683,12 @@ interface UseDocumentOptions<TData extends FirestoreObject> {
|
|
|
619
683
|
* })
|
|
620
684
|
*
|
|
621
685
|
* function ProjectEditor({ projectId }: { projectId: string }) {
|
|
622
|
-
* const { data, update,
|
|
686
|
+
* const { data, update, isLoaded } = useDocument({
|
|
623
687
|
* definition: projectDoc,
|
|
624
688
|
* params: { projectId },
|
|
625
689
|
* })
|
|
626
690
|
*
|
|
627
|
-
* if (
|
|
691
|
+
* if (!isLoaded) return <Spinner />
|
|
628
692
|
*
|
|
629
693
|
* return (
|
|
630
694
|
* <input
|
|
@@ -669,7 +733,7 @@ interface UseCollectionOptions<TData extends FirestoreObject> {
|
|
|
669
733
|
undoable?: boolean;
|
|
670
734
|
/**
|
|
671
735
|
* If false, no subscription is created and a no-op handle is returned
|
|
672
|
-
* (`{ data: {},
|
|
736
|
+
* (`{ data: {}, isLoaded: false, isActive: false }`). Use this to gate on
|
|
673
737
|
* route params that aren't ready yet. Default: true.
|
|
674
738
|
*/
|
|
675
739
|
enabled?: boolean;
|
|
@@ -708,8 +772,13 @@ interface UseCollectionOptions<TData extends FirestoreObject> {
|
|
|
708
772
|
* route params aren't ready yet).
|
|
709
773
|
*
|
|
710
774
|
* **SSR.** On the server there is no Firestore listener, so this hook returns
|
|
711
|
-
* the initial handle (`{ data: {},
|
|
712
|
-
*
|
|
775
|
+
* the initial handle (`{ data: {}, isLoaded: false }`, `isActive: false` for
|
|
776
|
+
* lazy). Avoid calling mutations server-side.
|
|
777
|
+
*
|
|
778
|
+
* Like {@link useDocument}, the default handle is **sync-agnostic** — `data`,
|
|
779
|
+
* `isLoaded`, `isActive`, `error`, but not `isSynced`. `isActive` stays so a
|
|
780
|
+
* lazy collection can gate a "Load" button; `isLoaded` is `isActive &&
|
|
781
|
+
* !isLoading`. Render save state via `use{Name}SyncStatus`.
|
|
713
782
|
*
|
|
714
783
|
* @example
|
|
715
784
|
* ```tsx
|
|
@@ -719,7 +788,7 @@ interface UseCollectionOptions<TData extends FirestoreObject> {
|
|
|
719
788
|
* })
|
|
720
789
|
*
|
|
721
790
|
* function SpacesList({ projectId }: { projectId: string }) {
|
|
722
|
-
* const { data, update, load, isActive,
|
|
791
|
+
* const { data, update, load, isActive, isLoaded } = useCollection({
|
|
723
792
|
* definition: spacesCollection,
|
|
724
793
|
* params: { projectId },
|
|
725
794
|
* })
|
|
@@ -728,7 +797,7 @@ interface UseCollectionOptions<TData extends FirestoreObject> {
|
|
|
728
797
|
* useEffect(() => { load() }, [load])
|
|
729
798
|
*
|
|
730
799
|
* if (!isActive) return <Button onClick={load}>Load Spaces</Button>
|
|
731
|
-
* if (
|
|
800
|
+
* if (!isLoaded) return <Spinner />
|
|
732
801
|
*
|
|
733
802
|
* return (
|
|
734
803
|
* <ul>
|
|
@@ -758,6 +827,78 @@ declare function useCollection<TData extends FirestoreObject>(options: UseCollec
|
|
|
758
827
|
* ```
|
|
759
828
|
*/
|
|
760
829
|
declare function useCollection<TData extends FirestoreObject, TSelected>(options: UseCollectionOptions<TData> & CollectionSelectorOptions<TData, TSelected>): SelectedCollectionHandle<TData, TSelected>;
|
|
830
|
+
/** Options for the document status hooks (a subset of {@link UseDocumentOptions}). */
|
|
831
|
+
interface UseDocumentStatusOptions<TData extends FirestoreObject> {
|
|
832
|
+
/** Document definition from defineDocument(). */
|
|
833
|
+
definition: DocumentDefinition<TData>;
|
|
834
|
+
/** Route/path parameters for dynamic paths. */
|
|
835
|
+
params?: Record<string, string>;
|
|
836
|
+
/**
|
|
837
|
+
* If false, no subscription is created and the idle status is returned
|
|
838
|
+
* (`{ isSynced: true, isSaving: false }` / `{ isLoading: false, isLoaded:
|
|
839
|
+
* false }`). Default: true.
|
|
840
|
+
*/
|
|
841
|
+
enabled?: boolean;
|
|
842
|
+
}
|
|
843
|
+
/** Options for the collection status hooks. Adds `queryConstraints`. */
|
|
844
|
+
interface UseCollectionStatusOptions<TData extends FirestoreObject> {
|
|
845
|
+
/** Collection definition from defineCollection(). */
|
|
846
|
+
definition: CollectionDefinition<TData>;
|
|
847
|
+
/** Route/path parameters for dynamic paths. */
|
|
848
|
+
params?: Record<string, string>;
|
|
849
|
+
/**
|
|
850
|
+
* Query constraints. Must produce the same query the data hook uses, or the
|
|
851
|
+
* status hook resolves a *different* shared entry (a second listener) —
|
|
852
|
+
* sharing is keyed by semantic query identity.
|
|
853
|
+
*/
|
|
854
|
+
queryConstraints?: QueryConstraint[];
|
|
855
|
+
/** See {@link UseDocumentStatusOptions.enabled}. */
|
|
856
|
+
enabled?: boolean;
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Subscribe to a document's **sync status only** — `{ isSynced, isSaving }`.
|
|
860
|
+
*
|
|
861
|
+
* The opt-in counterpart to the sync-agnostic default handle (see
|
|
862
|
+
* {@link DocumentHandle}): it re-renders when sync state flips but never on data
|
|
863
|
+
* changes, and shares the resource's one `onSnapshot` listener with
|
|
864
|
+
* `useDocument` and any slice hooks, so opting in adds no listener. While
|
|
865
|
+
* disabled it reports `{ isSynced: true, isSaving: false }`.
|
|
866
|
+
*/
|
|
867
|
+
declare function useDocumentSyncStatus<TData extends FirestoreObject>(options: UseDocumentStatusOptions<TData>): SyncStatus;
|
|
868
|
+
/**
|
|
869
|
+
* Subscribe to a document's **loading status only** — `{ isLoading, isLoaded }`.
|
|
870
|
+
*
|
|
871
|
+
* A spinner channel that shares the resource's listener and does NOT re-render
|
|
872
|
+
* on data changes — for a progress indicator rendered apart from the data. The
|
|
873
|
+
* data handle keeps `isLoaded` for the common render path; this is an extra
|
|
874
|
+
* channel, not a replacement.
|
|
875
|
+
*/
|
|
876
|
+
declare function useDocumentLoadingStatus<TData extends FirestoreObject>(options: UseDocumentStatusOptions<TData>): LoadingStatus;
|
|
877
|
+
/**
|
|
878
|
+
* Collection counterpart of {@link useDocumentSyncStatus} — `{ isSynced,
|
|
879
|
+
* isSaving }` over a collection query, sharing its one listener.
|
|
880
|
+
*
|
|
881
|
+
* **Lazy caveat.** On a `lazy` collection this hook never calls `load()` itself:
|
|
882
|
+
* activating a lazy listener is the data hook's job, and a passive status reader
|
|
883
|
+
* must not silently start the listener (and bill the reads) the laziness exists
|
|
884
|
+
* to defer. As the *lone* subscriber it therefore attaches no listener and stays
|
|
885
|
+
* at the idle `{ isSynced: true, isSaving: false }`. Mount it alongside a
|
|
886
|
+
* {@link useCollection} on the same query whose `load()` has run — the status
|
|
887
|
+
* hook rides that one shared listener and reports real sync state. Non-lazy
|
|
888
|
+
* collections activate on mount, so this hook works standalone there.
|
|
889
|
+
*/
|
|
890
|
+
declare function useCollectionSyncStatus<TData extends FirestoreObject>(options: UseCollectionStatusOptions<TData>): SyncStatus;
|
|
891
|
+
/**
|
|
892
|
+
* Collection counterpart of {@link useDocumentLoadingStatus} — `{ isLoading,
|
|
893
|
+
* isLoaded }` over a collection query, sharing its one listener.
|
|
894
|
+
*
|
|
895
|
+
* Same lazy caveat as {@link useCollectionSyncStatus}: on a `lazy` collection it
|
|
896
|
+
* never calls `load()`, so as the lone subscriber it attaches no listener and
|
|
897
|
+
* stays at the idle `{ isLoading: false, isLoaded: false }` until a co-mounted
|
|
898
|
+
* {@link useCollection} (or any active hook on the same query) activates the
|
|
899
|
+
* shared listener via `load()`. Non-lazy collections activate on mount.
|
|
900
|
+
*/
|
|
901
|
+
declare function useCollectionLoadingStatus<TData extends FirestoreObject>(options: UseCollectionStatusOptions<TData>): LoadingStatus;
|
|
761
902
|
/**
|
|
762
903
|
* Keyboard shortcut hook for undo/redo
|
|
763
904
|
*
|
|
@@ -1036,7 +1177,21 @@ type SelectedParams<P extends string, PExtra> = IsAny<PExtra> extends true ? Par
|
|
|
1036
1177
|
type SelectedDocHookFor<T extends FirestoreObject, P extends string, PExtra, TSelected> = keyof SelectedParams<P, PExtra> extends never ? (params?: Record<string, string>, options?: DocHookOptions<T>) => SelectedDocumentHandle<T, TSelected> : (params: SelectedParams<P, PExtra>, options?: DocHookOptions<T>) => SelectedDocumentHandle<T, TSelected>;
|
|
1037
1178
|
type SelectedColHookFor<T extends FirestoreObject, P extends string, PExtra, TSelected> = keyof SelectedParams<P, PExtra> extends never ? (params?: Record<string, string>, options?: ColHookOptions<T>) => SelectedCollectionHandle<T, TSelected> : (params: SelectedParams<P, PExtra>, options?: ColHookOptions<T>) => SelectedCollectionHandle<T, TSelected>;
|
|
1038
1179
|
type HookFor<E> = E extends SelectedDocEntry<infer T, infer P, infer PExtra, infer TSelected> ? SelectedDocHookFor<T, P, PExtra, TSelected> : E extends SelectedColEntry<infer T, infer P, infer PExtra, infer TSelected> ? SelectedColHookFor<T, P, PExtra, TSelected> : E extends DocEntry<infer T, infer P> ? keyof ParamsOf<P> extends never ? DocHookOptionalParams<T> : DocHookRequiredParams<T, P> : E extends ColEntry<infer T, infer P> ? keyof ParamsOf<P> extends never ? ColHookOptionalParams<T> : ColHookRequiredParams<T, P> : never;
|
|
1039
|
-
type
|
|
1180
|
+
type SyncStatusHookName<K extends string> = `${HookName<K>}SyncStatus`;
|
|
1181
|
+
type LoadingStatusHookName<K extends string> = `${HookName<K>}LoadingStatus`;
|
|
1182
|
+
type BaseEntry = DocEntry<any, any> | ColEntry<any, any>;
|
|
1183
|
+
type DocStatusHookOptions = {
|
|
1184
|
+
enabled?: boolean;
|
|
1185
|
+
};
|
|
1186
|
+
type ColStatusHookOptions = {
|
|
1187
|
+
enabled?: boolean;
|
|
1188
|
+
queryConstraints?: QueryConstraint[];
|
|
1189
|
+
};
|
|
1190
|
+
type DocStatusHookFor<P extends string, Ret> = keyof ParamsOf<P> extends never ? (params?: Record<string, string>, options?: DocStatusHookOptions) => Ret : (params: ParamsOf<P>, options?: DocStatusHookOptions) => Ret;
|
|
1191
|
+
type ColStatusHookFor<P extends string, Ret> = keyof ParamsOf<P> extends never ? (params?: Record<string, string>, options?: ColStatusHookOptions) => Ret : (params: ParamsOf<P>, options?: ColStatusHookOptions) => Ret;
|
|
1192
|
+
type SyncStatusHookFor<E> = E extends DocEntry<any, infer P> ? DocStatusHookFor<P, SyncStatus> : E extends ColEntry<any, infer P> ? ColStatusHookFor<P, SyncStatus> : never;
|
|
1193
|
+
type LoadingStatusHookFor<E> = E extends DocEntry<any, infer P> ? DocStatusHookFor<P, LoadingStatus> : E extends ColEntry<any, infer P> ? ColStatusHookFor<P, LoadingStatus> : never;
|
|
1194
|
+
type FirestateApi<R extends FirestateRegistry> = { [K in keyof R & string as HookName<K>]: HookFor<R[K]> } & { [K in keyof R & string as R[K] extends BaseEntry ? SyncStatusHookName<K> : never]: SyncStatusHookFor<R[K]> } & { [K in keyof R & string as R[K] extends BaseEntry ? LoadingStatusHookName<K> : never]: LoadingStatusHookFor<R[K]> };
|
|
1040
1195
|
/**
|
|
1041
1196
|
* Turn a Firestate registry into a map of typed React hooks. Each entry
|
|
1042
1197
|
* `K` produces a hook named `use{Capitalize<K>}`.
|
|
@@ -1489,5 +1644,5 @@ declare const FirestateStoreProvider: React.FC<FirestateStoreProviderProps>;
|
|
|
1489
1644
|
*/
|
|
1490
1645
|
declare const useUnsavedChangesBlocker: () => boolean;
|
|
1491
1646
|
//#endregion
|
|
1492
|
-
export { type AnySelectedEntry, type ColEntry, type CollectionDefinition, type CollectionHandle, type CollectionOptions, type CollectionSelectorOptions, type CollectionState, type DeepPartial, type DocEntry, type DocumentDefinition, type DocumentHandle, type DocumentOptions, type DocumentSelectorOptions, type DocumentState, type ErrorContext, type FirestateApi, type FirestateConfig, FirestateContext, type FirestateEntry, FirestateProvider, type FirestateProviderProps, type FirestateRegistry, type FirestateStore, FirestateStoreProvider, type FirestateStoreProviderProps, type FirestoreObject, type InferCollectionData, type InferCollectionDocument, type InferDocument, type InferDocumentData, type SelectOptions, type SelectedColEntry, type SelectedCollectionHandle, type SelectedDocEntry, type SelectedDocumentHandle, type Store, type UndoAction, type UndoManager, type UndoManagerConfig, type UndoManagerState, type UndoManagerWithSubscribe, type UpdateOptions, type UseCollectionOptions, type UseDocumentOptions, applyDiff, applyDiffMutable, col, computeDiff, computeUndoDiff, createCollectionSubscription, createDiffAtPath, createDocumentSubscription, createFirestate, createStore, createUndoManager, deepClone, defineCollection, defineDocument, diffContainsPath, doc, extractDiffValue, flattenDiff, flattenDiffToFieldPaths, isDeepEqual, isDiffEmpty, mergeDiffs, shallow, unflattenDiff, useCollection, useDocument, useIsSynced, useStore, useUndoKeyboardShortcuts, useUndoManager, useUnsavedChangesBlocker };
|
|
1647
|
+
export { type AnySelectedEntry, type ColEntry, type CollectionDefinition, type CollectionHandle, type CollectionOptions, type CollectionSelectorOptions, type CollectionState, type DeepPartial, type DocEntry, type DocumentDefinition, type DocumentHandle, type DocumentOptions, type DocumentSelectorOptions, type DocumentState, type ErrorContext, type FirestateApi, type FirestateConfig, FirestateContext, type FirestateEntry, FirestateProvider, type FirestateProviderProps, type FirestateRegistry, type FirestateStore, FirestateStoreProvider, type FirestateStoreProviderProps, type FirestoreObject, type InferCollectionData, type InferCollectionDocument, type InferDocument, type InferDocumentData, type LoadingStatus, type SelectOptions, type SelectedColEntry, type SelectedCollectionHandle, type SelectedDocEntry, type SelectedDocumentHandle, type Store, type SyncStatus, type UndoAction, type UndoManager, type UndoManagerConfig, type UndoManagerState, type UndoManagerWithSubscribe, type UpdateOptions, type UseCollectionOptions, type UseCollectionStatusOptions, type UseDocumentOptions, type UseDocumentStatusOptions, applyDiff, applyDiffMutable, col, computeDiff, computeUndoDiff, createCollectionSubscription, createDiffAtPath, createDocumentSubscription, createFirestate, createStore, createUndoManager, deepClone, defineCollection, defineDocument, diffContainsPath, doc, extractDiffValue, flattenDiff, flattenDiffToFieldPaths, isDeepEqual, isDiffEmpty, mergeDiffs, shallow, unflattenDiff, useCollection, useCollectionLoadingStatus, useCollectionSyncStatus, useDocument, useDocumentLoadingStatus, useDocumentSyncStatus, useIsSynced, useStore, useUndoKeyboardShortcuts, useUndoManager, useUnsavedChangesBlocker };
|
|
1493
1648
|
//# sourceMappingURL=index.d.mts.map
|