Skip to content

feat: Add useSuspenseQuery and useSuspenseObservableQuery hooks with QueryBoundary for React Suspense support#1870

Merged
einari merged 6 commits intomainfrom
copilot/investigate-usequery-and-observablequery
Mar 16, 2026
Merged

feat: Add useSuspenseQuery and useSuspenseObservableQuery hooks with QueryBoundary for React Suspense support#1870
einari merged 6 commits intomainfrom
copilot/investigate-usequery-and-observablequery

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 8, 2026

Adds Suspense-compatible variants of the existing query hooks, enabling components to leverage React's <Suspense> boundaries for loading states and QueryErrorBoundary / QueryBoundary for error handling. Generated proxy classes also expose useSuspense() static methods for a consistent API.

Added

  • useSuspenseQuery / useSuspenseQueryWithPaging — suspends the component while the HTTP query is in-flight; throws QueryFailed or QueryUnauthorized on failure so a QueryErrorBoundary can catch them
  • useSuspenseObservableQuery / useSuspenseObservableQueryWithPaging — suspends until the first WebSocket message arrives, then streams updates reactively via useState; same error propagation
  • QueryFailed — error type carrying exceptionMessages and exceptionStackTrace from server-side exceptions
  • QueryUnauthorized — error type for unauthorized responses
  • QueryErrorBoundary — class-based React ErrorBoundary that catches QueryFailed and QueryUnauthorized; exposes an onError render-prop receiving a typed QueryErrorInfo object (error, isQueryFailed, isQueryUnauthorized, reset)
  • QueryBoundary — convenience wrapper combining <Suspense> and QueryErrorBoundary in a single component; accepts loadingFallback for the loading state
  • clearSuspenseQueryCache() / clearSuspenseObservableQueryCache() — cache invalidation helpers intended for test teardown
  • useSuspense() / useSuspenseWithPaging() static methods on all generated query proxies (both Query.hbs and ObservableQuery.hbs templates), forwarding to the new hooks
  • Storybook stories for useSuspenseQuery (stories/queries/SuspenseQuery.stories.tsx) with four scenarios: happy path (loading → data table with re-run button), server exception (QueryFailed caught by QueryErrorBoundary), unauthorized (QueryUnauthorized caught by QueryErrorBoundary), and QueryBoundary combined usage
  • Storybook stories for useSuspenseObservableQuery (stories/queries/SuspenseObservableQuery.stories.tsx) with two scenarios: streaming updates (auto-updates every 2 s with inject-event and re-run buttons), and server exception (QueryFailed caught by QueryErrorBoundary)
  • Documentation/frontend/react/suspense-queries.md — full reference documentation covering how Suspense hooks work, QueryErrorBoundary and QueryBoundary components with prop tables, all four hook signatures and return values, QueryFailed / QueryUnauthorized error type tables, proxy static methods, a complete end-to-end example with QueryBoundary, a comparison table vs. the standard hooks, and cache/re-fetching guidance including test teardown
function ItemList() {
    const [result] = useSuspenseQuery(ItemsQuery, { filter: 'active' });
    return <ul>{result.data.map(i => <li key={i.id}>{i.name}</li>)}</ul>;
}

// Wrap with QueryBoundary — handles both loading and error states:
<QueryBoundary
    loadingFallback={<Spinner />}
    onError={({ isQueryUnauthorized, reset }) =>
        isQueryUnauthorized
            ? <p>Not authorized. <button onClick={reset}>Retry</button></p>
            : <p>Something went wrong. <button onClick={reset}>Retry</button></p>
    }
>
    <ItemList />
</QueryBoundary>

// Or using the generated proxy static method directly:
function ItemList() {
    const [result] = ItemsQuery.useSuspense({ filter: 'active' });
    return <ul>{result.data.map(i => <li key={i.id}>{i.name}</li>)}</ul>;
}

Changed

  • queries/index.ts — re-exports the four new hooks, the two error types, QueryErrorBoundary, QueryBoundary, and QueryErrorInfo
  • Query.hbs proxy template — updated imports and added useSuspense() / useSuspenseWithPaging() static methods
  • ObservableQuery.hbs proxy template — updated imports and added useSuspense() / useSuspenseWithPaging() static methods
  • Documentation/frontend/react/queries.md — updated introduction to reference Suspense hooks and added a "Suspense-Compatible Hooks" section linking to the new documentation page
  • Documentation/frontend/react/proxy-generation.md — added "Suspense Hooks on Proxies" subsection under Queries showing useSuspense() and QueryBoundary usage
  • Documentation/frontend/react/toc.yml — added "Suspense Queries" navigation entry between Queries and Proxy Generation

Fixed

  • React 18.3 resets all hook state (useRef, useMemo, useState) on every Suspense retry for uncommitted components. The hooks use a module-level cache keyed on query class + config + args so the pending Promise and resolved result survive across retries without triggering an infinite fetch loop.
Original prompt

This section details on the original issue you should resolve

<issue_title>Investigate if our useQuery() & useObservableQuery() hooks work with React Suspense and ErrorBoundaries as expected</issue_title>
<issue_description>Look into React - use for more reference</issue_description>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

…eact Suspense and ErrorBoundary support

Co-authored-by: einari <134365+einari@users.noreply.github.com>
Copilot AI changed the title [WIP] Investigate useQuery and useObservableQuery hooks with React Suspense feat: Add useSuspenseQuery and useSuspenseObservableQuery hooks for React Suspense and ErrorBoundary support Mar 8, 2026
@einari
Copy link
Copy Markdown
Contributor

einari commented Mar 8, 2026

@copilot The proxies generated should have a ‘useSuspense()’ static method on them as well, that forwards to these new hooks

@einari
Copy link
Copy Markdown
Contributor

einari commented Mar 8, 2026

@copilot add Storybook stories that showcases these capabilities - create a query that overrides the query provider and simulates delay. In the stories, add a button for re-running the queries

…ates

Co-authored-by: einari <134365+einari@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 8, 2026

@copilot The proxies generated should have a ‘useSuspense()’ static method on them as well, that forwards to these new hooks

Done in 2efa3ae. Both Query.hbs and ObservableQuery.hbs proxy templates now generate useSuspense() and useSuspenseWithPaging() static methods alongside the existing use() / useWithPaging() methods. The new imports (useSuspenseQuery, useSuspenseQueryWithPaging, useSuspenseObservableQuery, useSuspenseObservableQueryWithPaging) are also added to the respective templates.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • example.com
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node node /home/REDACTED/work/Arc/Arc/node_modules/.bin/vitest run ensions/when_converting_empty_string_to_pascal_case.cs ensions/when_converting_pascal_case_to_kebab_case.cs ensions/when_converting_lowercase_string_to_pascal_case.cs ensions/when_converting_single_word_to_kebab_case.cs ensions/when_converting_null_string_to_kebab_case.cs ensions/when_converting_empty_string_to_kebab_case.cs ensions/when_converting_pascal_case_to_camel_case.cs xtensions/SampleType.cs xtensions/when_checking_is_optional/with_non_nullable_string_property.cs xten�� xtensions/when_checking_is_optional/with_nullable_string_property.cs xtensions/TypeWithEnumerableProperty.cs l.d.ts leProps.ts .ts ptions.ts ging/for_Messeng/home/REDACTED/work/Arc/Arc/Source/DotNET/Arc.Core.Specs/Authorization/for_AuthorizationEvaluator/given/TypeWithAuthorizationAndAllowAnonymous.cs (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

…vableQuery

Co-authored-by: einari <134365+einari@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 8, 2026

@copilot add Storybook stories that showcases these capabilities - create a query that overrides the query provider and simulates delay. In the stories, add a button for re-running the queries

Done in 701e4c7. Two story files were added under stories/queries/:

SuspenseQuery.stories.tsx — three stories:

  • Happy Path — component suspends ~1.5s while a DelayedTodoQuery (overrides perform() with a setTimeout) resolves, then renders a data table. A Re-run query button clears the cache and remounts the subtree to replay the loading sequence.
  • Server Exception (QueryFailed)FailingTodoQuery returns hasExceptions: true; the hook throws QueryFailed which the inline ErrorBoundary catches and displays. A Retry button resets the boundary.
  • Unauthorized (QueryUnauthorized) — same pattern with isAuthorized: falseQueryUnauthorized.

SuspenseObservableQuery.stories.tsx — two stories:

  • Streaming UpdatesStreamingLogQuery (overrides subscribe()) pushes initial entries after 1.5s then auto-updates every 2s. Inject event adds a manual log entry; Re-run query clears the observable cache and resets the stream.
  • Server Exception (QueryFailed)FailingLogQuery pushes a hasExceptions: true response; the ErrorBoundary catches the thrown QueryFailed.

Both stories use ArcContext.Provider with a local config (no real network calls since perform/subscribe are overridden on the fake query classes).

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • example.com
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node node /home/REDACTED/work/Arc/Arc/node_modules/.bin/vitest run (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@einari
Copy link
Copy Markdown
Contributor

einari commented Mar 9, 2026

@copilot Add documentation for the new suspense hooks and also for the proxy hooks as well

…eQuery hooks

Co-authored-by: einari <134365+einari@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 9, 2026

@copilot Add documentation for the new suspense hooks and also for the proxy hooks as well

Done in af131c9. The following documentation was added/updated:

  • Documentation/frontend/react/suspense-queries.md (new) — full reference page covering: how the throw-a-Promise protocol works, useSuspenseQuery / useSuspenseQueryWithPaging and useSuspenseObservableQuery / useSuspenseObservableQueryWithPaging signatures and examples, QueryFailed / QueryUnauthorized error types with property tables, useSuspense() / useSuspenseWithPaging() proxy static methods, a complete end-to-end example with ErrorBoundary, a comparison table vs. the standard hooks, and cache/re-fetching guidance including test teardown.
  • Documentation/frontend/react/queries.md — updated intro to list Suspense hooks alongside the standard ones, and added a "Suspense-Compatible Hooks" section with a link to the new page.
  • Documentation/frontend/react/proxy-generation.md — added a "Suspense Hooks on Proxies" subsection under the Queries section showing useSuspense() usage and linking to the new page.
  • Documentation/frontend/react/toc.yml — added a "Suspense Queries" entry between Queries and Proxy Generation.

@einari
Copy link
Copy Markdown
Contributor

einari commented Mar 9, 2026

@copilot We've now fixed what we needed to fix in Chronicle and upgraded to the latest version in Arc. What I don't see now, is how we can really hook this up in Arc now. Are we missing something vital to be able to do this?

…pense integration

Co-authored-by: einari <134365+einari@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 9, 2026

@copilot We've now fixed what we needed to fix in Chronicle and upgraded to the latest version in Arc. What I don't see now, is how we can really hook this up in Arc now. Are we missing something vita...

The vital missing piece was that Arc had no ErrorBoundary component of its own — users had to write a class-based one themselves (as the Storybook stories were doing locally). Added in 666c1c2:

  • QueryErrorBoundary — a class-based React ErrorBoundary that catches QueryFailed and QueryUnauthorized. Exposes an onError render-prop receiving a QueryErrorInfo object (error, isQueryFailed, isQueryUnauthorized, and a reset() callback to re-mount the subtree):
<QueryErrorBoundary
    onError={({ isQueryUnauthorized, error, reset }) =>
        isQueryUnauthorized
            ? <p>Not authorized. <button onClick={reset}>Retry</button></p>
            : <p>{error.message} <button onClick={reset}>Retry</button></p>
    }
>
    <Suspense fallback={<Spinner />}>
        <ItemList />
    </Suspense>
</QueryErrorBoundary>
  • QueryBoundary — combines <Suspense> + QueryErrorBoundary in one component so the setup is now a single wrapper:
<QueryBoundary
    loadingFallback={<Spinner />}
    onError={({ isQueryUnauthorized, reset }) => ...}
>
    <ItemList />
</QueryBoundary>

Both are exported from @cratis/arc.react/queries. The Storybook stories were also refactored to use these components (removing their inline ErrorBoundary classes), and the documentation was updated with a full "Boundary Components" section.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • example.com
    • Triggering command: /opt/hostedtoolcache/node/24.14.0/x64/bin/node /opt/hostedtoolcache/node/24.14.0/x64/bin/node /home/REDACTED/work/Arc/Arc/node_modules/vitest/vitest.mjs run (dns block)
    • Triggering command: /opt/hostedtoolcache/node/24.14.0/x64/bin/node /opt/hostedtoolcache/node/24.14.0/x64/bin/node /home/REDACTED/work/Arc/Arc/node_modules/vitest/vitest.mjs run Linux-1.0.9/bin/security (dns block)
    • Triggering command: /opt/hostedtoolcache/node/24.14.0/x64/bin/node /opt/hostedtoolcache/node/24.14.0/x64/bin/node /home/REDACTED/work/Arc/Arc/node_modules/vitest/vitest.mjs run git conf�� ries/SuspenseObservableQuery.stories.tsx user.email bash (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title feat: Add useSuspenseQuery and useSuspenseObservableQuery hooks for React Suspense and ErrorBoundary support feat: Add useSuspenseQuery and useSuspenseObservableQuery hooks with QueryBoundary for React Suspense support Mar 9, 2026
@github-actions
Copy link
Copy Markdown

NuGet packages for this PR, e.g. Cratis.Arc:
https://github.com/cratis/arc/packages/1655206?version=19.9.8-pr1870.666c1c2

einari added a commit that referenced this pull request Mar 16, 2026
19.10.0 release branch: merge PRs #1869, #1870, #1888 with build and test fixes
@einari einari merged commit 623d328 into main Mar 16, 2026
14 of 16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Investigate if our useQuery() & useObservableQuery() hooks work with React Suspense and ErrorBoundaries as expected

2 participants