Skip to content

Add dynamic flag to opt provided variables out of value memoization#5342

Open
striedinger wants to merge 1 commit into
facebook:mainfrom
striedinger:provided-variable-dynamic
Open

Add dynamic flag to opt provided variables out of value memoization#5342
striedinger wants to merge 1 commit into
facebook:mainfrom
striedinger:provided-variable-dynamic

Conversation

@striedinger

@striedinger striedinger commented Jun 25, 2026

Copy link
Copy Markdown

Summary

Provided-variable providers are memoized process-wide. withProvidedVariables
pins each provider's first return value in a module-level cache keyed by the
get function, to enforce purity. That's fine in a browser tab, but wrong on a
server, where one process serves many requests/viewers. A provider that reads
per-request state (auth/session, locale, experiment bucket) legitimately returns
different values per request, and today that causes two problems:

  1. The first request's value is pinned and reused for every later request, so
    subsequent requests resolve with stale variables.
  2. Dev builds emit a spurious pure-function warning, since the value differs
    across requests:
Relay: Expected function `get` for provider `…` to be a pure function, but got conflicting return values `…` and `…`

Note the store/reader path (RelayConcreteVariables.getOperationVariables)
already resolves providers fresh per operation with no cache, so the two
resolution paths disagree for any non-pure provider.

This adds an opt-in dynamic?: boolean to the provider object. When dynamic
is true, withProvidedVariables uses the freshly resolved value and skips both
the cache and the purity warning. Default behavior is unchanged.

// Viewer_IsLoggedIn.relayprovider.js
export default {
  dynamic: true, // resolved fresh on every operation rather than memoized
  get() {
    return getCurrentViewer().isLoggedIn;
  },
};

dynamic is a runtime property of the provider module, so this needs no new
GraphQL surface. It does require a small relay-compiler change: the generated
provider type is an exact object ({ readonly get: () => T }), so the compiler
now also emits an optional readonly dynamic?: boolean. Without it, a
.relayprovider module that exports dynamic fails Flow ("exact objects do not
accept extra props").

Changes

  • relay-runtime/util/withProvidedVariables.js — bypass the memoization cache
    (and its warning) for dynamic providers.
  • relay-runtime/util/RelayConcreteNode.js / .d.ts — add
    readonly dynamic?: boolean to the provider type (Flow + TS).
  • relay-typegen (write.rs) — emit dynamic?: boolean on the generated
    provider type; typegen snapshots and generated artifacts regenerated.
  • Docs (graphql-directives.mdx) — document dynamic and add an example.
  • Test + provideDynamicValue.relayprovider fixture.

Notes

  • The provider's get() is resolved on both the store path
    (getOperationVariables) and the network path (withProvidedVariables); this
    duplication is pre-existing. For dynamic providers it makes the two paths
    more consistent (both fresh) rather than less. get() should remain
    consistent within a single run.

Test Plan

  • yarn jest packages/relay-runtime/util/__tests__/withProvidedVariables-test.js
    — new "marked dynamic" test (fresh value each call, no warning) passes;
    existing cache/warning tests unchanged.
  • yarn jest ProvidedVariable — all provided-variable suites pass (regenerated
    artifacts are a types-only change).
  • yarn flow check — No errors.
  • yarn prettier — clean.
  • Rust: cargo fmt clean for write.rs; cargo test -p relay-typegen and the
    compile_relay_artifacts targets pass against the blessed snapshots; fixtures
    regenerated via UPDATE_SNAPSHOTS=1.

Provided-variable providers are memoized process-wide: withProvidedVariables
pins each provider's first return value in a module-level cache to enforce
purity. On a server, where one process serves many requests/viewers, a provider
that reads per-request state legitimately varies per request, which pins stale
values and emits spurious purity warnings.

Add an opt-in `dynamic?: boolean` on the provider object. When true,
withProvidedVariables uses the freshly resolved value and skips both the cache
and the warning; default behavior is unchanged. relay-typegen now emits an
optional `dynamic?: boolean` on the generated provider type so a provider
exporting it still typechecks against the exact object type. Docs, tests, and a
fixture are included; generated artifacts and typegen snapshots regenerated.
@meta-cla

meta-cla Bot commented Jun 25, 2026

Copy link
Copy Markdown

Hi @striedinger!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@striedinger striedinger marked this pull request as ready for review June 25, 2026 21:22
@meta-cla meta-cla Bot added the CLA Signed label Jun 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant