diff --git a/.changeset/client-react-bulk-data-hooks.md b/.changeset/client-react-bulk-data-hooks.md new file mode 100644 index 0000000000..f5c73af2f9 --- /dev/null +++ b/.changeset/client-react-bulk-data-hooks.md @@ -0,0 +1,32 @@ +--- +"@objectstack/client-react": minor +--- + +feat(client-react): bulk-write hooks, and `useAutoRefresh` now refreshes on predicate writes (#4678) + +#4639 gave predicate writes (`multi: true` update/delete) their own event +contract — `data.records.updated` / `data.records.deleted`, carrying a +`matched` count and no record — and `@objectstack/client` exposes them via +`subscribeBulkData`. The React hooks never caught up: all three realtime data +hooks delegated to `subscribeData`, so React consumers could not see bulk +writes at all. + +The sharpest edge was **`useAutoRefresh`**. Its whole job is "refetch when the +data changes", and a predicate write is the case that dirties a list hardest — +one statement can change or delete every row on screen. It sat still for those +while refetching dutifully for a single-row edit. + +- **New `useBulkDataSubscription(object)`** returning the latest + `BulkDataEvent`, and **`useBulkDataSubscriptionCallback(object, cb)`** for + the refetch/side-effect case. +- **`useAutoRefresh` now watches both streams.** Safe here in a way it is not + for `useDataSubscription`: this hook's output is a refetch signal, not an + event body, so the shape difference that keeps the two contracts apart never + reaches the caller. When `options.recordId` narrows it to one record it still + refetches on a bulk event — a count cannot say whether that record was in the + match set, and a redundant query beats showing a row a predicate write + already changed. +- **`useDataSubscription` / `useDataSubscriptionCallback` are unchanged** and + still per-record only. Their callbacks are typed `(event: DataEvent) => void`; + letting a `BulkDataEvent` through would hand them an object whose `recordId` + and record body are `undefined` — the defect #4626 removed. diff --git a/packages/client-react/src/index.tsx b/packages/client-react/src/index.tsx index 5e46a95938..b5ff1f85c4 100644 --- a/packages/client-react/src/index.tsx +++ b/packages/client-react/src/index.tsx @@ -53,6 +53,8 @@ export { useDataSubscription, useMetadataSubscriptionCallback, useDataSubscriptionCallback, + useBulkDataSubscription, + useBulkDataSubscriptionCallback, useRealtimeConnection, useAutoRefresh } from './realtime-hooks'; diff --git a/packages/client-react/src/realtime-hooks.tsx b/packages/client-react/src/realtime-hooks.tsx index 470cb74775..e57ec36251 100644 --- a/packages/client-react/src/realtime-hooks.tsx +++ b/packages/client-react/src/realtime-hooks.tsx @@ -8,7 +8,7 @@ */ import { useEffect, useState, useCallback } from 'react'; -import type { MetadataEvent, DataEvent } from '@objectstack/spec/api'; +import type { MetadataEvent, DataEvent, BulkDataEvent } from '@objectstack/spec/api'; import { useClient } from './context'; /** @@ -192,6 +192,96 @@ export function useDataSubscriptionCallback( }, [client, object, callback, options?.recordId]); } +/** + * Hook to subscribe to bulk (predicate-write) data events + * + * A `multi: true` update/delete reaches the driver's `updateMany`/`deleteMany`, + * which report an affected COUNT and name no rows — so it publishes + * `data.records.updated` / `data.records.deleted` rather than the per-record + * events {@link useDataSubscription} delivers (#4639). + * + * The event carries `object` and `matched` — there is no `recordId` and no + * record body, which is why this is a separate hook rather than more types + * flowing through `useDataSubscription`: a `DataEvent` callback receiving one + * of these would read `undefined` for every field it expects. + * + * Use it to invalidate a list, show "40 records changed", or trigger a + * refetch — not to patch a per-record cache, which a count cannot drive. + * + * @param object - Object name to subscribe to + * @returns Latest bulk data event or null + * + * @example + * ```tsx + * function TaskList() { + * const bulk = useBulkDataSubscription('project_task'); + * + * useEffect(() => { + * if (bulk) { + * console.log(`${bulk.matched} tasks changed in one write`); + * } + * }, [bulk]); + * + * return