Skip to content

@defer'd fragment returns partial data instead of suspending when the parent record is populated by a sibling non-deferred query #5357

Description

@jonreading81

Version

react-relay / relay-runtime 21.0.1, with React 19 streaming SSR + @defer.

Problem

Under React 19 streaming SSR of a query with @defer, useFragment reads partial data instead of suspending, while the underlying OperationExecutor is still emitting incremental payloads. Consumers that expect the fragment's fields to be either fully present (fragment resolved) or absent (fragment suspended) crash on undefined subtree fields.

Fix proposed in #5360.

Root cause

fetchQueryDeduped caches a per-identifier subject and tears it down when subscriber count hits 0. Under React 19 streaming SSR, that cleanup fires between Suspense render passes for a query still emitting @defer chunks — the subject is torn down while the executor is alive. From that moment on:

  1. getPromiseForActiveRequest returns null (the cache entry has been deleted).
  2. RelayOperationTracker.getPendingOperationsAffectingOwner returns null (no other operation writes to the owner's records — this is the specifically-broken case when a sibling non-deferred query has already populated the shared record).
  3. getPendingOperationsForFragment returns null.
  4. useFragment reads the partial snapshot and returns non-suspended data.

The condition surfaces most obviously when a sibling non-deferred query has already populated the shared record — viewer.me exists, but viewer.me.commentsReceived (owned by the deferred fragment) does not — because that's the state where partial-data reads look "consistent enough" to render but crash on a specific field. Structurally, the underlying gap (request-cache drained between Suspense passes) affects any streaming operation whose executor outlives its cache subject.

Minimal shape

Two queries preloaded on the same page.

Query A (non-deferred, populates viewer.me in the store):

query SiblingQuery {
    viewer {
        me {
            username
            hasProFeatures
        }
    }
}

Query B (defers a fragment that reads other fields on the same viewer.me):

query MyDashboardQuery($itemCount: Int!) {
    viewer {
        ...LatestCommentsList_viewer @defer @arguments(itemCount: $itemCount)
    }
}

fragment LatestCommentsList_viewer on Viewer @argumentDefinitions(itemCount: {type: "Int!"}) {
    me {
        commentsReceived(first: $itemCount) {
            edges { ...CommentCardList_commentEdges }
        }
    }
}

SiblingQuery completes first, populating viewer.me. During SSR of the consumer of LatestCommentsList_viewer:

  • viewer → present
  • viewer.me → present (from SiblingQuery)
  • viewer.me.commentsReceivedundefined (still in the deferred chunk of MyDashboardQuery)

useFragment returns { me: { …siblingFields }, commentsReceived: undefined } instead of suspending.

Repro fingerprint

Switched to client rendering because the server rendering errored:
TypeError: Cannot read properties of undefined (reading 'edges')
    at LatestCommentsList (…/index.jsx:59:34)

Expected behaviour

useFragment for a fragment whose data has not yet arrived (spread was @defer'd, no deferred chunk applied) should suspend regardless of whether the parent record identity exists in the store from another query. Otherwise every consumer must defensively null-guard every field their fragment selects, defeating the point of generated non-null types.

Fix

#5360 — introduces a per-environment registry of operation completion promises tied to OperationExecutor lifetime, independent of the request-cache subject and its subscriber count. getPendingOperationsForFragment consults it as a last-resort correlation. Gated behind RelayFeatureFlags.ENABLE_IN_FLIGHT_OPERATION_CORRELATION (default off).

Related

#5354 — distinct-but-related: @defer on a fragment-nested spread that uses @connection leaves the connection's edges empty.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions