Skip to content

Make @module work out of the box#5332

Draft
captbaritone wants to merge 5 commits into
mainfrom
relay-3d-out-of-box
Draft

Make @module work out of the box#5332
captbaritone wants to merge 5 commits into
mainfrom
relay-3d-out-of-box

Conversation

@captbaritone

Copy link
Copy Markdown
Contributor

Summary

Makes Relay's data-driven dependencies (@module) work without any special configuration beyond what's in the quick start guide. Each commit removes one piece of boilerplate:

  1. Baseline e2e test — establishes the starting point with all manual wiring
  2. Default OperationLoaderEnvironment auto-creates one; removes custom operationLoader + Store construction
  3. Prefetch component at normalization time — eliminates waterfall where component only loads at render
  4. Default MatchContainer loader — uses the environment's OperationLoader + Suspense; removes custom loader prop
  5. Export MatchContainer — importable from react-relay directly

Also switches e2e test infra to native ESM for TypeScript fixtures (needed for clean import() behavior).

Before

const operationLoader = { get() {...}, load() {...} };
const store = new Store(new RecordSource(), { operationLoader });
const env = new Environment({ network, operationLoader, store });

const lazyCache = new Map();
function moduleLoader(ref) { /* React.lazy wrapper */ }

<MatchContainer match={data.field} loader={moduleLoader} />

After

const env = new Environment({ network });

<MatchContainer match={data.field} />

Test plan

  • All 12 e2e tests pass at each commit
  • Snapshot shows correct rendered output with no console warnings
  • Backward compatible: explicit operationLoader and loader prop still work when provided

Demonstrates the minimal setup for Relay 3D with the current compiler:
- No JSDependency scalar or js() field needed — the compiler generates
  componentModuleProvider/operationModuleProvider as inline dynamic imports
- OperationLoader calls the operationModuleProvider to async-load split
  normalization ASTs
- MatchContainer loader wraps componentModuleProvider with React.lazy for
  Suspense-based code splitting
- Server schema is just a plain union, no special 3D infrastructure

Also switches the e2e test infra to native ESM for TypeScript fixtures:
- extensionsToTreatAsEsm: ['.ts', '.tsx'] in jest config
- Skip CJS module transform for TS fixture files in jest-transform
- Use import() instead of require() in the test harness
- NODE_OPTIONS=--experimental-vm-modules in the test:e2e script
- eagerEsModules: true for babel-plugin-relay

This eliminates CJS/ESM interop issues where import() of Babel-transformed
modules produced nested { default: { default: Component } }.
Environment now auto-creates an OperationLoader when none is provided.
The default handles the () => import(...) function references that the
compiler generates for componentModuleProvider/operationModuleProvider,
with a cache so that get() returns results synchronously after the first
async load() — needed by DataChecker and RelayReferenceMarker.

Users no longer need to define a custom operationLoader or construct a
Store explicitly to use @module. createOperationLoader is also exported
from relay-runtime for users who construct their own Store.
When componentModuleProvider is a function (the () => import(...) generated
by the compiler), call it eagerly during normalization to start loading the
component module in parallel with the operation normalization AST. Module
systems cache import() calls, so when MatchContainer's loader calls the
same function at render time it resolves from the cache — eliminating the
waterfall where the component only starts loading after render.
When no loader is provided, MatchContainer resolves componentModuleProvider
functions using the environment's OperationLoader — the same cache that
powers normalization AST loading. This means:

- get() returns the component synchronously if already cached (e.g. from
  the normalization-time prefetch added in the previous commit)
- If not cached, a Suspense-compatible load is triggered

Users no longer need to define a custom loader function or manage a
React.lazy cache. The loader prop is still accepted for custom resolution
strategies.
MatchContainer is now importable directly from react-relay instead of
the deep path react-relay/relay-hooks/MatchContainer. Added to both
index.js and hooks.js exports.
@meta-cla meta-cla Bot added the CLA Signed label Jun 17, 2026
@meta-codesync

meta-codesync Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

@captbaritone has imported this pull request. If you are a Meta employee, you can view this in D108909152.

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