@pylonsync/react 0.3.265 → 0.3.267

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +3 -3
  2. package/src/hooks.ts +27 -24
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.3.265",
6
+ "version": "0.3.267",
7
7
  "type": "module",
8
8
  "main": "src/index.ts",
9
9
  "types": "src/index.ts",
@@ -12,8 +12,8 @@
12
12
  "check": "tsc -p tsconfig.json --noEmit"
13
13
  },
14
14
  "dependencies": {
15
- "@pylonsync/sdk": "0.3.265",
16
- "@pylonsync/sync": "0.3.265"
15
+ "@pylonsync/sdk": "0.3.267",
16
+ "@pylonsync/sync": "0.3.267"
17
17
  },
18
18
  "peerDependencies": {
19
19
  "react": ">=19.0.0"
package/src/hooks.ts CHANGED
@@ -74,14 +74,17 @@ export function useQuery<T = Row>(
74
74
  entity: string,
75
75
  options?: QueryOptions
76
76
  ): UseQueryReturn<T> {
77
- // `loading` is true only while we genuinely don't know yet — i.e.,
78
- // the engine hasn't drained IndexedDB into the in-memory store. Once
79
- // hydrated, an empty list is a real empty (not "still fetching"), so
80
- // refreshes with cached data don't flash "Loading…" before the rows
81
- // render. The engine's `isHydrated()` exposes a synchronous gate that
82
- // flips true after `loadAllEntities()` settles.
77
+ // `loading` is true only while we genuinely don't know yet — i.e., the
78
+ // engine doesn't yet have a SERVER-confirmed view. It gates on
79
+ // `isInitialSyncSettled()`, NOT `isHydrated()`: the latter flips true the
80
+ // instant IndexedDB loads, which on a cold/empty cache (first visit, or
81
+ // right after an org switch wipes the replica) is immediate and EMPTY — so
82
+ // gating on it drops `loading` while the rows are still en route from the
83
+ // server, flashing the empty state for the seconds until the snapshot lands.
84
+ // `isInitialSyncSettled()` stays pending until the first pull settles (or
85
+ // the cache already had rows), so callers render a skeleton instead.
83
86
  const loading = useRef<boolean>(
84
- !sync.isHydrated() && sync.store.list(entity).length === 0,
87
+ !sync.isInitialSyncSettled() && sync.store.list(entity).length === 0,
85
88
  );
86
89
  const error = useRef<Error | null>(null);
87
90
  const optionsKey = JSON.stringify(options || {});
@@ -112,14 +115,13 @@ export function useQuery<T = Row>(
112
115
  if (sig !== snapshotCache.current.sig) {
113
116
  snapshotCache.current = { rows: filtered as T[], sig };
114
117
  }
115
- // Loading is false the moment hydration finishes even when the
116
- // disk replica is empty (a real "no rows yet" state, not "still
117
- // fetching"). The previous gate only flipped when rows arrived,
118
- // so an entity with zero cached rows stayed in loading=true
119
- // forever until a server pull / WS event populated it. After:
120
- // hydration completion fires store.notify() getSnapshot re-runs
121
- // loading goes false immediately, the empty state renders.
122
- if (loading.current && (rows.length > 0 || sync.isHydrated())) {
118
+ // Drop loading when rows arrive OR the initial sync settles (first server
119
+ // pull done / cache had rows / fallback deadline). Gating on the settled
120
+ // signal not bare hydration is what stops the empty-state flash: a
121
+ // cold cache keeps loading=true until the pull confirms, then an empty
122
+ // result is a real "no rows" and the empty state renders. No flash, and
123
+ // (thanks to the fallback) no infinite spinner either.
124
+ if (loading.current && (rows.length > 0 || sync.isInitialSyncSettled())) {
123
125
  loading.current = false;
124
126
  }
125
127
  return snapshotCache.current.rows;
@@ -167,11 +169,12 @@ export function useQueryOne<T = Row>(
167
169
  entity: string,
168
170
  id: string
169
171
  ): UseQueryOneReturn<T> {
170
- // Same hydration-aware loading semantics as useQuery — see the
171
- // comment there. Without this, a refreshed page flashes "Loading…"
172
- // for the row even when it's already in IndexedDB.
172
+ // Same initial-sync-aware loading semantics as useQuery — see the comment
173
+ // there. Gates on isInitialSyncSettled() (server-confirmed) so a cold load
174
+ // shows a skeleton rather than flashing "not found" before the pull lands,
175
+ // and a refreshed page doesn't flash "Loading…" when the row is cached.
173
176
  const loading = useRef<boolean>(
174
- !sync.isHydrated() && sync.store.get(entity, id) === null,
177
+ !sync.isInitialSyncSettled() && sync.store.get(entity, id) === null,
175
178
  );
176
179
  const error = useRef<Error | null>(null);
177
180
 
@@ -197,11 +200,11 @@ export function useQueryOne<T = Row>(
197
200
  if (sig !== snapshotCache.current.sig) {
198
201
  snapshotCache.current = { row: (row as T) ?? null, sig };
199
202
  }
200
- // Mirror useQuery: loading flips false on hydration completion
201
- // even when the row is missing (not "still fetching" genuinely
202
- // not present yet). Without the isHydrated() check, a missing
203
- // row stuck loading=true forever.
204
- if (loading.current && (row !== null || sync.isHydrated())) {
203
+ // Mirror useQuery: loading flips false once the row arrives OR the initial
204
+ // sync settles (server-confirmed). Gating on isInitialSyncSettled()not
205
+ // bare hydration keeps a cold load in the skeleton state until the pull
206
+ // confirms, so a row that exists server-side doesn't flash "not found".
207
+ if (loading.current && (row !== null || sync.isInitialSyncSettled())) {
205
208
  loading.current = false;
206
209
  }
207
210
  return snapshotCache.current.row;