Skip to content

feat(react): support activity component/data preload outside React runtime context#716

Merged
ENvironmentSet merged 15 commits into
mainfrom
feature/fep-2357
Jun 8, 2026
Merged

feat(react): support activity component/data preload outside React runtime context#716
ENvironmentSet merged 15 commits into
mainfrom
feature/fep-2357

Conversation

@ENvironmentSet
Copy link
Copy Markdown
Collaborator

@ENvironmentSet ENvironmentSet commented Jun 4, 2026

Problem

Preloading an activity's resources before entering it — the lazy component chunk and the data described by the activity's loader — is currently only possible through the usePrepare hook.

Because usePrepare is a React hook, it depends on React Context (useConfig, useDataLoader, useActivityComponentMap) and can therefore only be called inside a mounted React tree. This makes it impossible to warm up chunks/data at the moments that matter most for initial-entry performance:

  • app bootstrap, before the first render

In practice, consumers who need render-ahead preloading have had to reach for private APIs (e.g. the lazy component's internal _load), which is fragile and unsupported.

Solution

Expose the same preload capability as a plain function on the stackflow() output, alongside the existing render-outside APIs (actions, stepActions):

const { Stack, actions, stepActions, prepare } = stackflow({
  config,
  components,
  plugins,
});

// outside React — bootstrap
prepare("Article", { articleId: "123" }); // warm chunk + fire data loader
prepare("Article");                       // warm chunk only

Design decisions:

  • Same instance, single source. prepare comes from the same stackflow() call as Stack/actions, so config and components never have to be passed twice and can never drift from the running instance. Since it does not touch the core store, it is usable from module-evaluation time — before <Stack> is mounted.
  • Unchanged signature. The existing Prepare type is reused as-is: omitting activityParams preloads only the component chunk; passing params also fires the activity's data loader. The returned Promise<void> resolves when all fired preload work settles.
  • Cache warming, not data injection. prepare does not store loader results; injecting loaderData into activities remains the loader plugin's responsibility.
  • Failure semantics. Failures (unregistered activity, loader/chunk errors) are delivered as a rejection of the returned promise with the original reason — never a synchronous throw. A failed chunk load is retried on a subsequent prepare, so transient failures don't poison the cache. Fire-and-forget callers should attach .catch.
  • usePrepare becomes a thin wrapper over the same implementation, so existing in-tree callers keep working unchanged.
  • Type safety preserved. RegisteredActivityName / InferActivityParams<K> flow through unchanged, so invalid activity names or params remain compile-time errors.

🤖 Generated with Claude Code

ENvironmentSet and others added 6 commits June 4, 2026 15:26
- Add Jest + @swc/jest + jsdom + Testing Library to integrations/react,
  following the inline-config convention of plugin-blocker/plugin-history-sync
- Exclude *.spec.* files from esbuild/dts build output; add tsconfig.test.json
  so `yarn typecheck` covers spec files (incl. Register augmentations)
- Type-only cast in PluginRenderer so library source stays type-checkable
  when specs augment the Register interface
- Add a harness smoke spec using an inline renderer plugin (public render API)
  to avoid a workspace dependency cycle with plugin-renderer-basic

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- FEP-2357-SPEC.md: Linear issue + locked interface design, plus spec-owner
  decisions (reject semantics, original-reason propagation, retry after
  failure; loader dedupe / chunk duplicate firing / atomicity left unspecified)
- FEP-2357-TEST-PLAN.md: 32 given-when-then test items (A-G), approved by
  test reviewer after two review rounds

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Translates FEP-2357-TEST-PLAN.md items A2-A9, B1-B3, C1-C2, E1-E10 and
F1-F2 into Jest specs against the public entry (./index). `prepare` is
not implemented yet, so all 25 tests fail with "prepare is not a
function" — verified red for the right reason by temporarily wiring a
reference implementation (all green) and reverting it.

Register augmentation uses optional params only ({ id?: string });
registering required params breaks package-internal typecheck variance
in stackflow.tsx/useStepFlow.ts. Because Register merges globally,
every stackflow() call passes a complete components map via
baseComponents spread.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
D1-D2 verify the current usePrepare behavior that the new prepare must
match (spec §3 "thin wrapper"). These run against existing code and
pass today.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
G1-G4 are typecheck-only assertions placed in never-called function
bodies (swc does not typecheck; runtime execution must be avoided).
A1 lives here because Jest requires at least one test per spec file.

Until prepare lands, `yarn workspace @stackflow/react typecheck` fails
with TS2339 (Property 'prepare' does not exist on StackflowOutput) in
both this file and prepare.spec.tsx — a single root cause. Verified
that a typed reference implementation turns typecheck fully green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The smoke spec invited removal once real specs cover the same ground;
prepare.spec.tsx/usePrepare.spec.tsx now exercise the same harness
surface (spec pickup, @swc/jest, jsdom + Testing Library, workspace
deps, inline renderer plugin).

Keeping it would also break typecheck: Register augmentation merges
package-wide, so its stackflow() call would need components for every
Prepare* activity registered by the new specs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jun 4, 2026

🦋 Changeset detected

Latest commit: 2c19d56

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@stackflow/react Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 4, 2026

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added prepare function to the stackflow output, callable outside React components for preloading activity components and data
    • Accepts activity name and optional parameters; returns a promise with async error handling
  • Refactor

    • usePrepare hook now delegates to shared implementation, maintaining existing behavior

Walkthrough

The PR extracts shared activity preparation logic into a reusable makePrepare factory and exposes it as a new prepare function on the stackflow() output, allowing component preload and data loader triggering outside React. The existing usePrepare hook is refactored to wrap this shared implementation.

Changes

Prepare API implementation and integration

Layer / File(s) Summary
Type contract and public export
integrations/react/src/Prepare.ts, integrations/react/src/index.ts
Defines the generic Prepare type (activity name + optional inferred params → Promise) and re-exports it from the package API.
Prepare factory implementation
integrations/react/src/makePrepare.ts
Implements makePrepare factory that resolves activity configs, conditionally prefetches data via loaders and component _load methods, and preloads structured component content, aggregating all prefetch operations via Promise.all.
Prepare wired into stackflow output
integrations/react/src/stackflow.tsx
Imports makePrepare and integrates prepare into StackflowOutput type and stackflow() function return, wiring the factory with config, loadData callback, and activity components.
Refactor usePrepare to wrap shared logic
integrations/react/src/usePrepare.ts
Replaces inline prepare implementation with a memoized makePrepare call, consolidating all prefetch orchestration into the shared factory and removing duplicate logic.
API addition documentation
.changeset/fep-2357-prepare-outside-react.md
Changeset describing the new prepare API behavior, parity with usePrepare semantics (no params warms only; params warm + fire loader), and promise-based error handling.

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: exposing a prepare function on the stackflow output for preloading activity components/data outside React context.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the problem solved, solution implemented, and design decisions with code examples.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/fep-2357

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Jun 4, 2026

@stackflow/link

yarn add https://pkg.pr.new/@stackflow/link@716.tgz

@stackflow/plugin-basic-ui

yarn add https://pkg.pr.new/@stackflow/plugin-basic-ui@716.tgz

@stackflow/plugin-blocker

yarn add https://pkg.pr.new/@stackflow/plugin-blocker@716.tgz

@stackflow/plugin-google-analytics-4

yarn add https://pkg.pr.new/@stackflow/plugin-google-analytics-4@716.tgz

@stackflow/plugin-history-sync

yarn add https://pkg.pr.new/@stackflow/plugin-history-sync@716.tgz

@stackflow/plugin-lifecycle

yarn add https://pkg.pr.new/@stackflow/plugin-lifecycle@716.tgz

@stackflow/plugin-renderer-basic

yarn add https://pkg.pr.new/@stackflow/plugin-renderer-basic@716.tgz

@stackflow/plugin-renderer-web

yarn add https://pkg.pr.new/@stackflow/plugin-renderer-web@716.tgz

@stackflow/react-ui-core

yarn add https://pkg.pr.new/@stackflow/react-ui-core@716.tgz

@stackflow/react

yarn add https://pkg.pr.new/@stackflow/react@716.tgz

commit: f70e16e

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Jun 4, 2026

Deploying stackflow-demo with  Cloudflare Pages  Cloudflare Pages

Latest commit: f70e16e
Status: ✅  Deploy successful!
Preview URL: https://6d3065fa.stackflow-demo.pages.dev
Branch Preview URL: https://feature-fep-2357.stackflow-demo.pages.dev

View logs

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Jun 4, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
stackflow-docs f70e16e Commit Preview URL Jun 08 2026, 01:31 PM

@ENvironmentSet ENvironmentSet changed the title feat(react): support activity component/data preload outside React context (FEP-2357) feat(react): support activity component/data preload outside React context Jun 4, 2026
@ENvironmentSet ENvironmentSet changed the title feat(react): support activity component/data preload outside React context feat(react): support activity component/data preload outside React runtime context Jun 4, 2026
ENvironmentSet and others added 9 commits June 7, 2026 21:18
…an/spec docs

Working code is the source of truth: test-case rationale now lives as
self-contained comments in the spec files (contract summary + unspecified-
behavior guardrails in headers, given/when/then per test), with provenance
pointing to Linear FEP-2357. Process artifacts (harness setup, fixture
sketches, self-audit checklists) belong in the PR/Linear, not the repo.

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

The A1-G4 numbering was an artifact of the deleted test plan — an index-
less numbering scheme that rots on insertion. Test titles are the
identifiers now; cross-references are descriptive; "unspecified in spec"
phrasing becomes "not a contract" so comments presume no external doc.
Issue provenance stays in commit messages (FEP-2357) per repo convention.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The compile-time guarantees follow from reusing the existing Prepare
type (RegisteredActivityName / InferActivityParams generics already
exercised across the codebase), and the output-shape runtime check was
redundant — every runtime spec destructures and calls prepare anyway.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reuses the public Prepare type so the compile-time contract (activity
name / params inference) is fixed ahead of implementation and the spec
files typecheck green. The stub throws explicitly, keeping the runtime
specs red with a uniform, unambiguous reason until a worker fills in
the implementation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
stackflow() owns the prepare contract; usePrepare is the wrapper. The
type now lives in Prepare.ts (same pattern as Actions/StepActions) and
both sides reference it, instead of the core importing it from the
hook. Also drop the implementation-guidance comment from the stub —
the specs alone define what to build.

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

The "separation of responsibility" header line now says what it means:
prepare only fires work; loaderData injection and lazy rendering stay
with the existing navigation path (prepare → push behaves like plain
push). The core-store-non-touching line is rephrased as the observable
capability it grants: prepare is usable before the core store
initializes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract the usePrepare body into a context-free `makePrepare` factory that
takes the three stackflow() inputs it actually depends on (config, loadData,
activityComponentMap) and returns the `Prepare` function. Wire it as the
`prepare` field of the stackflow() output so chunk/data preloading can run
before the React tree mounts, and refactor `usePrepare` into a thin wrapper
over the same implementation so in-tree callers are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the Jest test facility and the prepare/usePrepare spec suites that were
set up for FEP-2357, returning all test-only infrastructure to its main-branch
state:

- delete `prepare.spec.tsx`, `usePrepare.spec.tsx`, and `tsconfig.test.json`
- restore `package.json` (remove the `test` script, jest config, and test
  devDependencies; revert `typecheck` to `tsc --noEmit`)
- restore `esbuild.config.js` (revert the `*.spec.*` entry-point exclusion to
  the plain `./src/**/*` glob) and `tsconfig.json` (drop the spec excludes)
- restore `PluginRenderer.tsx` (the spec-motivated `RegisteredActivityName`
  cast is no longer needed)
- restore `yarn.lock` / `.pnp.cjs` to drop the test dependencies

The `prepare` implementation (makePrepare, the stackflow() wiring, the
usePrepare wrapper, the Prepare type export) and its changeset are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ENvironmentSet ENvironmentSet marked this pull request as ready for review June 8, 2026 14:29
@ENvironmentSet ENvironmentSet enabled auto-merge (squash) June 8, 2026 14:29
@ENvironmentSet ENvironmentSet disabled auto-merge June 8, 2026 14:31
@ENvironmentSet ENvironmentSet merged commit 510a287 into main Jun 8, 2026
8 of 9 checks passed
@ENvironmentSet ENvironmentSet deleted the feature/fep-2357 branch June 8, 2026 14:31
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