Skip to content

feat(strawberry-shake): persist Razor query components across prerender#1

Closed
cliedeman wants to merge 1 commit into
mainfrom
feat/razor-persistent-component-state
Closed

feat(strawberry-shake): persist Razor query components across prerender#1
cliedeman wants to merge 1 commit into
mainfrom
feat/razor-persistent-component-state

Conversation

@cliedeman

Copy link
Copy Markdown

Summary

Generated Blazor query components can now survive the server prerender → interactive boundary using PersistentComponentState, so the query is not re-executed on the client after a prerender. This removes the double fetch and the loading-state flash for InteractiveServer / InteractiveAuto / prerendered WASM.

Opt-in; queries and store-backed clients only. Output is unchanged when the feature is off.

Relates to ChilliCream#7209.

How it works

  • CaptureOperationResultBuilder.Build clones the raw transport data element into IOperationResult.ContextData[WellKnownContextData.PersistedData], gated by a new CapturePersistedData virtual. The generated JsonResultBuilder overrides it to true only when the feature is on (generated output is byte-identical when off).
  • Persist — the new UsePersistentQuery<TResult> Razor base awaits the first result on the cold path (so prerendered HTML contains data), then RegisterOnPersisting saves the payload under Operation.GetPersistenceKey(args) (a stable OperationRequest.GetHash()).
  • Rehydrate — on interactive init, TryTakeFromJsonOperation.Watch(args, persistedState)OperationExecutor.Watch(request, persistedState, …) rebuilds the typed result via BuildFromPersistedData (seeding IEntityStore), seeds the operation store, and forces CacheFirst → served from cache, no network call.

Opt-in

.graphqlrc.json:

{ "extensions": { "strawberryShake": { "razorComponents": true, "razorPersistedState": true } } }

Notable decisions

  • Forces CacheFirst on the persisted path (the default strategy is NetworkOnly, which would otherwise refetch).
  • OnParametersSet re-subscribes only when the operation arguments change — supports dynamic parameters and avoids re-subscribing on every re-render.
  • BuildFromPersistedData is a throwing default-interface-method on IOperationResultBuilder, so non-JSON transport builders are unaffected.
  • Gated off under NoStore (the storeless executor throws; generators skip the wiring). Subscriptions and mutations are untouched.

Tests

  • Core.Tests: capture into ContextData, gating-off, BuildFromPersistedData round-trip fidelity, rehydrate-without-network (asserts the connection is never called), and the storeless guard.
  • Razor codegen snapshot: RazorGeneratorTests.Query_With_Persisted_State (the snapshot test compiles the generated code against the runtime).
  • StrawberryShake.Client and StrawberryShake.CodeGeneration solutions build clean across net8.0/net9.0/net10.0/net11.0.

Known limitations

  • Targets the initial prerender → interactive load (all TFMs). The richer net10+ enhanced-navigation APIs (RegisterOnRestoring / AllowUpdates) are not used.
  • The mutation result builder also emits the CapturePersistedData override (harmless; nothing reads it for mutations). Gating to query result builders only is not cleanly available from ResultBuilderDescriptor.
  • No bUnit/host integration test yet; the no-refetch contract is proven at the runtime layer.

🤖 Generated with Claude Code

Generated Blazor query components can now survive the server prerender to
interactive boundary using PersistentComponentState, so the query is not
re-executed on the client after a prerender (no double fetch, no loading
flash).

- OperationResultBuilder captures the raw transport "data" payload into
  ContextData (gated by the CapturePersistedData virtual) and can rebuild a
  result from a persisted payload via BuildFromPersistedData.
- OperationExecutor.Watch gains a persisted-state overload that seeds the
  store from the payload and serves the result from the cache (CacheFirst),
  skipping the network. StorelessOperationExecutor throws (store required).
- New UsePersistentQuery<TResult> Razor base awaits the first result on the
  cold path, persists it during prerender, rehydrates it on interactive
  init, and re-subscribes only when the operation arguments change.
- RazorQueryGenerator and OperationServiceGenerator emit the wiring behind a
  new opt-in RazorPersistedState setting (queries and store-backed clients
  only). Output is unchanged when the setting is off.

Tests: Core.Tests cover capture, gating, BuildFromPersistedData round-trip,
rehydrate-without-network, and the storeless guard; a new Razor codegen
snapshot covers the generated component and operation members.

Relates to ChilliCream#7209.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cliedeman

Copy link
Copy Markdown
Author

Closing as the wrong direction, superseded by #2.

The declarative [PersistentState] serializer in #2 covers the prerender → interactive hydration need without a parallel component base (UsePersistentQuery), an OperationExecutor.Watch persisted-state overload, or the extra generator emission this branch added. The shared foundation (raw-payload capture, BuildFromPersistedData, the RazorPersistedState opt-in, the capture override) lives in #2.

The only capability this branch had that #2 doesn't is reactive store-subscription updates after hydration (the Watch model) and net8/9 support. If reactive components ever need persisted state, the path is to enhance the existing UseQuery<T> on top of #2's foundation rather than revive this parallel base.

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.

1 participant