@syncular/react 0.3.0 → 0.3.1

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.
@@ -157,23 +157,35 @@ export function useRawSql(sql, params, options) {
157
157
  };
158
158
  }, [client, key, enabled, tick]);
159
159
  // Subscribe once per client/enabled/query identity; a matching event asks the
160
- // scheduler for a run (coalesced). `key`/`scopeKeysKey` re-key the sub.
160
+ // scheduler for a run (coalesced). `key`/`scopeKeysKey` re-key the sub. The
161
+ // scheduler is read from the ref AT EVENT TIME (never captured at effect
162
+ // setup): the lifecycle effect below may replace it on a remount, and a
163
+ // captured disposed instance would swallow every schedule() silently.
161
164
  // biome-ignore lint/correctness/useExhaustiveDependencies: key/scopeKeysKey re-key the subscription intentionally
162
165
  useEffect(() => {
163
166
  if (!enabled)
164
167
  return;
165
- const scheduler = schedulerRef.current;
166
168
  const unsubscribe = client.onInvalidate((event) => {
167
169
  if (eventMatches(event, depTablesRef.current, scopeKeysRef.current)) {
168
- scheduler?.schedule();
170
+ schedulerRef.current?.schedule();
169
171
  }
170
172
  });
171
173
  return unsubscribe;
172
174
  }, [client, enabled, key, scopeKeysKey]);
173
- // Dispose the scheduler when the hook unmounts (drop any pending frame).
175
+ // Scheduler lifecycle. Under StrictMode (and any future fiber remount)
176
+ // React runs mount → cleanup → mount on the SAME hook instance: the cleanup
177
+ // disposes the scheduler, so the setup must RE-CREATE it — the render-time
178
+ // lazy init above never runs again (the ref is non-undefined), and a
179
+ // disposed scheduler turns every later invalidation into a silent no-op
180
+ // that freezes the live query forever.
174
181
  useEffect(() => {
175
- const scheduler = schedulerRef.current;
176
- return () => scheduler?.dispose();
182
+ if (schedulerRef.current === undefined) {
183
+ schedulerRef.current = new FrameScheduler(() => runRef.current?.());
184
+ }
185
+ return () => {
186
+ schedulerRef.current?.dispose();
187
+ schedulerRef.current = undefined;
188
+ };
177
189
  }, []);
178
190
  return { rows, isLoading, error, refresh };
179
191
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/react",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "React hooks for Syncular offline-first sync",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Benjamin Kniffler",
@@ -200,23 +200,35 @@ export function useRawSql<Row = SqlRow>(
200
200
  }, [client, key, enabled, tick]);
201
201
 
202
202
  // Subscribe once per client/enabled/query identity; a matching event asks the
203
- // scheduler for a run (coalesced). `key`/`scopeKeysKey` re-key the sub.
203
+ // scheduler for a run (coalesced). `key`/`scopeKeysKey` re-key the sub. The
204
+ // scheduler is read from the ref AT EVENT TIME (never captured at effect
205
+ // setup): the lifecycle effect below may replace it on a remount, and a
206
+ // captured disposed instance would swallow every schedule() silently.
204
207
  // biome-ignore lint/correctness/useExhaustiveDependencies: key/scopeKeysKey re-key the subscription intentionally
205
208
  useEffect(() => {
206
209
  if (!enabled) return;
207
- const scheduler = schedulerRef.current;
208
210
  const unsubscribe = client.onInvalidate((event) => {
209
211
  if (eventMatches(event, depTablesRef.current, scopeKeysRef.current)) {
210
- scheduler?.schedule();
212
+ schedulerRef.current?.schedule();
211
213
  }
212
214
  });
213
215
  return unsubscribe;
214
216
  }, [client, enabled, key, scopeKeysKey]);
215
217
 
216
- // Dispose the scheduler when the hook unmounts (drop any pending frame).
218
+ // Scheduler lifecycle. Under StrictMode (and any future fiber remount)
219
+ // React runs mount → cleanup → mount on the SAME hook instance: the cleanup
220
+ // disposes the scheduler, so the setup must RE-CREATE it — the render-time
221
+ // lazy init above never runs again (the ref is non-undefined), and a
222
+ // disposed scheduler turns every later invalidation into a silent no-op
223
+ // that freezes the live query forever.
217
224
  useEffect(() => {
218
- const scheduler = schedulerRef.current;
219
- return () => scheduler?.dispose();
225
+ if (schedulerRef.current === undefined) {
226
+ schedulerRef.current = new FrameScheduler(() => runRef.current?.());
227
+ }
228
+ return () => {
229
+ schedulerRef.current?.dispose();
230
+ schedulerRef.current = undefined;
231
+ };
220
232
  }, []);
221
233
 
222
234
  return { rows, isLoading, error, refresh };