Skip to content

Conversation

@ENvironmentSet
Copy link
Collaborator

@ENvironmentSet ENvironmentSet commented Dec 1, 2025

React team is going to recommend passing synchronously inspectable promise to use for avoiding unnecessary fallback exposure. I followed their guidelines to optimize renderings incorporating data loaders/lazy components

This optimization matters since react applies default delay before removing fallbacks have shown.

@changeset-bot
Copy link

changeset-bot bot commented Dec 1, 2025

🦋 Changeset detected

Latest commit: 82ce76b

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

coderabbitai bot commented Dec 1, 2025

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Summary by CodeRabbit

  • Refactor
    • Optimized lazy component loading and promise handling for improved suspense fallback rendering.
    • Simplified type signatures for lazy activity components.
    • Enhanced internal utilities for better asynchronous state management.

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

Replaces React.lazy indirection with a preloadable lazy component and introduces SyncInspectablePromise and useThenable utilities; loader and lazy-loading code paths were updated to use synchronous promise inspection and preload semantics, and one legacy use() helper was removed.

Changes

Cohort / File(s) Summary
Promise inspection utilities
integrations/react/src/__internal__/utils/SyncInspectablePromise.ts
Adds SyncInspectablePromise type, PromiseStatus constants, PromiseState union, inspect(), resolve(), reject() and internal wrapper to make promises synchronously inspectable.
Preloadable lazy component
integrations/react/src/__internal__/utils/PreloadableLazyComponent.tsx
New factory preloadableLazyComponent() returning { Component, preload }, with cached loading promise and Component using useThenable to render a lazily-loaded default export.
Thenable bridge hook
integrations/react/src/__internal__/utils/useThenable.ts
New useThenable<T>(thenable: PromiseLike<T>): Awaited<T> that inspects a SyncInspectablePromise, returns value if fulfilled, throws reason if rejected, or throws the underlying promise if pending.
Lazy type adjustment
integrations/react/src/__internal__/LazyActivityComponentType.ts
Changes LazyActivityComponentType from React.LazyExoticComponent<...> wrapper to StaticActivityComponentType<T> & { _load?: ... } (removes React.lazy indirection in type).
Future lazy implementation
integrations/react/src/future/lazy.tsx
Replaces React.lazy usage with preloadableLazyComponent and _load-based preload inspection using SyncInspectablePromise/inspect.
Structured activity loading
integrations/react/src/__internal__/StructuredActivityComponentType.tsx
Replaces direct React.lazy logic with preloadableLazyComponent and cached content resolver using SyncInspectablePromise inspection and retry-on-reject behavior.
Loader plugin & wiring
integrations/react/src/future/loader/loaderPlugin.tsx, integrations/react/src/future/loader/useLoaderData.ts
Transitioned loader flows to SyncInspectablePromise usage: loadData now returns SyncInspectablePromise, initial loaderData wrapped via resolve(), checks use inspect()/PromiseStatus, and useLoaderData now uses useThenable(resolve(loaderData)).
Removed helper
integrations/react/src/future/loader/use.ts
Deleted legacy `use(promise: Promise
Minor cleanup
integrations/react/src/future/stackflow.tsx
Removed unused isEqual import.
Changeset
.changeset/happy-ghosts-peel.md
Adds changeset noting minor release bump and description of the inspectable promise optimization.

Sequence Diagram(s)

sequenceDiagram
  participant App as App / Router
  participant Loader as loaderPlugin
  participant Activity as Preloadable Component
  participant SIP as SyncInspectablePromise

  App->>Loader: request activity + initialLoaderData
  Loader->>SIP: resolve(initialLoaderData)  -- wrap loader data
  Loader->>Activity: call preload() (preloadableLazyComponent)
  Activity->>SIP: cachedLoad() → returns SyncInspectablePromise
  SIP-->>Activity: status (PENDING / FULFILLED / REJECTED)
  alt both fulfilled
    Activity->>App: provide Component (default export)
    Loader->>App: provide loaderData (fulfilled value)
  else pending
    SIP-->>App: throws pending promise -> React Suspense boundary suspends
  else rejected
    SIP-->>App: throws rejection reason -> error boundary handles
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Review focus:
    • correctness of SyncInspectablePromise wrapping, status transitions and reuse logic
    • preloadableLazyComponent cached promise refresh on rejection and Component rendering via useThenable
    • loaderPlugin changes: Promise inspection, Promise.allSettled/resolve usage, and changed loadData signature impacts
    • migration sites where use() was removed to ensure no regressions

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: implementing synchronously inspectable promises to optimize suspense fallback rendering in React.
Description check ✅ Passed The description is clearly related to the changeset, explaining the React team guidance being followed and the optimization benefits for suspense fallback handling.

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b7360c6 and 82ce76b.

📒 Files selected for processing (1)
  • integrations/react/src/future/loader/loaderPlugin.tsx (3 hunks)

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.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Dec 1, 2025

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 82ce76b Commit Preview URL Dec 16 2025, 07:03 AM

@ENvironmentSet ENvironmentSet changed the title feat(react): Make data loaders and lazy activity definitions to use synchronously inspectable promise to allow react to optimize suspense fallback rendering feat(react): utilize synchronously inspectable promise to optimize suspense fallback rendering Dec 1, 2025
@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Dec 9, 2025

Deploying stackflow-demo with  Cloudflare Pages  Cloudflare Pages

Latest commit: 7a93506
Status: ✅  Deploy successful!
Preview URL: https://9f97f3bf.stackflow-demo.pages.dev
Branch Preview URL: https://sync-promise-for-suspense-op.stackflow-demo.pages.dev

View logs

@pkg-pr-new
Copy link

pkg-pr-new bot commented Dec 9, 2025

commit: 82ce76b

@ENvironmentSet ENvironmentSet marked this pull request as ready for review December 12, 2025 07:59
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
integrations/react/src/__internal__/LazyActivityComponentType.ts (1)

3-6: Breaking change: Type no longer extends React.LazyExoticComponent.

As noted by the author, this is a breaking change. Consumers relying on LazyActivityComponentType being a React.LazyExoticComponent (e.g., for type checks or specific React.lazy behavior) will encounter type errors.

Ensure this breaking change is:

  1. Documented in release notes/changelog
  2. Reflected in a semver major version bump (or follows the project's versioning policy for breaking changes)
  3. Migration guidance is provided for affected consumers
🧹 Nitpick comments (2)
integrations/react/src/__internal__/StructuredActivityComponentType.tsx (1)

91-109: Redundant double-wrapping with liftValue.

On line 93, content() already returns a SyncInspectablePromise (from the caching logic at lines 42-54), so wrapping it again with liftValue is unnecessary when content is a function. Additionally, on lines 104-108, contentPromise is already a SyncInspectablePromise, so wrapping the .then() result with liftValue creates another layer of wrapping.

Consider simplifying:

 const { Component: ContentComponent } = preloadableLazyComponent(() => {
   const content = structuredActivityComponent.content;
-  const contentPromise = liftValue(
-    typeof content === "function" ? content() : { default: content },
-  );
+  const contentPromise =
+    typeof content === "function" ? content() : liftValue({ default: content });
   const state = inspect(contentPromise);

   if (state.status === PromiseStatus.FULFILLED) {
     return liftValue({
       default: state.value.default.component,
     });
   }

-  return liftValue(
-    contentPromise.then((value) => ({
+  return makeSyncInspectable(
+    contentPromise.then((value) => ({
       default: value.default.component,
-    })),
-  );
+    })),
+  ) as SyncInspectablePromise<{ default: Content<{}>["component"] }>;
 });

Alternatively, if the current approach is intentional for defensive coding, a brief comment would clarify the intent.

integrations/react/src/future/loader/loaderPlugin.tsx (1)

120-128: Consider extracting the nested ternary for readability.

The logic is correct but the nested ternary is dense. Consider extracting to a helper function or using early returns.

-    const lazyComponentPromise = liftValue(
-      isStructuredActivityComponent(matchActivityComponent) &&
-        typeof matchActivityComponent.content === "function"
-        ? matchActivityComponent.content()
-        : "_load" in matchActivityComponent &&
-            typeof matchActivityComponent._load === "function"
-          ? matchActivityComponent._load()
-          : undefined,
-    );
+    const getLazyComponentResult = () => {
+      if (
+        isStructuredActivityComponent(matchActivityComponent) &&
+        typeof matchActivityComponent.content === "function"
+      ) {
+        return matchActivityComponent.content();
+      }
+      if (
+        "_load" in matchActivityComponent &&
+        typeof matchActivityComponent._load === "function"
+      ) {
+        return matchActivityComponent._load();
+      }
+      return undefined;
+    };
+    const lazyComponentPromise = liftValue(getLazyComponentResult());
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0a55459 and bc9949c.

📒 Files selected for processing (7)
  • integrations/react/src/__internal__/LazyActivityComponentType.ts (1 hunks)
  • integrations/react/src/__internal__/StructuredActivityComponentType.tsx (3 hunks)
  • integrations/react/src/__internal__/utils/PreloadableLazyComponent.tsx (1 hunks)
  • integrations/react/src/__internal__/utils/SyncInspectablePromise.ts (1 hunks)
  • integrations/react/src/future/lazy.tsx (1 hunks)
  • integrations/react/src/future/loader/loaderPlugin.tsx (6 hunks)
  • integrations/react/src/future/stackflow.tsx (4 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Write source in TypeScript with strict typing enabled across the codebase

Files:

  • integrations/react/src/future/stackflow.tsx
  • integrations/react/src/__internal__/utils/PreloadableLazyComponent.tsx
  • integrations/react/src/__internal__/utils/SyncInspectablePromise.ts
  • integrations/react/src/future/lazy.tsx
  • integrations/react/src/future/loader/loaderPlugin.tsx
  • integrations/react/src/__internal__/StructuredActivityComponentType.tsx
  • integrations/react/src/__internal__/LazyActivityComponentType.ts
integrations/react/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Use .tsx files for React components and JSX in the React integration package

Files:

  • integrations/react/src/future/stackflow.tsx
  • integrations/react/src/__internal__/utils/PreloadableLazyComponent.tsx
  • integrations/react/src/future/lazy.tsx
  • integrations/react/src/future/loader/loaderPlugin.tsx
  • integrations/react/src/__internal__/StructuredActivityComponentType.tsx
🧬 Code graph analysis (6)
integrations/react/src/future/stackflow.tsx (1)
integrations/react/src/__internal__/utils/SyncInspectablePromise.ts (2)
  • SyncInspectablePromise (3-7)
  • liftValue (73-89)
integrations/react/src/__internal__/utils/SyncInspectablePromise.ts (1)
integrations/react/src/__internal__/utils/isPromiseLike.ts (1)
  • isPromiseLike (1-8)
integrations/react/src/future/lazy.tsx (4)
integrations/react/src/__internal__/StaticActivityComponentType.ts (1)
  • StaticActivityComponentType (3-4)
integrations/react/src/__internal__/LazyActivityComponentType.ts (1)
  • LazyActivityComponentType (3-6)
integrations/react/src/__internal__/utils/PreloadableLazyComponent.tsx (1)
  • preloadableLazyComponent (9-35)
integrations/react/src/__internal__/utils/SyncInspectablePromise.ts (1)
  • liftValue (73-89)
integrations/react/src/future/loader/loaderPlugin.tsx (2)
integrations/react/src/__internal__/utils/SyncInspectablePromise.ts (5)
  • SyncInspectablePromise (3-7)
  • liftValue (73-89)
  • inspect (29-49)
  • PromiseStatus (9-13)
  • PromiseStatus (14-14)
integrations/react/src/__internal__/StructuredActivityComponentType.tsx (1)
  • isStructuredActivityComponent (59-67)
integrations/react/src/__internal__/StructuredActivityComponentType.tsx (2)
integrations/react/src/__internal__/utils/SyncInspectablePromise.ts (5)
  • SyncInspectablePromise (3-7)
  • liftValue (73-89)
  • inspect (29-49)
  • PromiseStatus (9-13)
  • PromiseStatus (14-14)
integrations/react/src/__internal__/utils/PreloadableLazyComponent.tsx (1)
  • preloadableLazyComponent (9-35)
integrations/react/src/__internal__/LazyActivityComponentType.ts (1)
integrations/react/src/__internal__/StaticActivityComponentType.ts (1)
  • StaticActivityComponentType (3-4)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Workers Builds: stackflow-docs
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (22)
integrations/react/src/__internal__/StructuredActivityComponentType.tsx (2)

5-12: LGTM!

The imports are correctly organized, bringing in the necessary utilities for the new preloadable lazy loading pattern and synchronously inspectable promises.


36-54: LGTM! Well-structured caching with proper rejected-state refresh.

The caching logic correctly:

  • Returns a pre-resolved SyncInspectablePromise for static content via liftValue
  • Refreshes the cache when the previous promise was rejected
  • Maintains a single cached promise for successful loads
integrations/react/src/future/stackflow.tsx (4)

23-26: LGTM!

Appropriate imports for the new SyncInspectablePromise-based loader data handling.


67-74: LGTM!

The type changes correctly align the cache and loadData signature with the new SyncInspectablePromise pattern, enabling synchronous inspection of loader data state.


92-97: LGTM!

Wrapping the loader result with liftValue correctly handles both synchronous values and promises, converting them to SyncInspectablePromise.


127-131: Rethrowing in .then() rejection handler is a no-op for unhandled rejection propagation.

The throw error on line 130 inside the rejection callback won't propagate the error to callers of loadData() — it only causes the returned promise from .then() to reject (which is ignored). The original loaderData promise's rejection is already propagated. The throw here might be intentional to surface the error in dev tools, but if so, a comment would clarify intent.

Is the throw error intentional for logging/debugging, or should it be removed since loaderData already propagates rejections to consumers?

integrations/react/src/future/lazy.tsx (2)

3-4: LGTM!

Correctly imports the new preloadable lazy loading utilities.


9-22: LGTM! Clean refactor to preloadable pattern.

The implementation correctly:

  • Uses preloadableLazyComponent with liftValue(load()) to wrap the loader
  • Exposes _load that awaits preload() and returns the wrapped Component
  • Maintains the LazyActivityComponentType<T> contract
integrations/react/src/__internal__/utils/PreloadableLazyComponent.tsx (1)

9-34: LGTM! Well-designed preloadable lazy component pattern.

The implementation correctly:

  • Caches the loading promise and refreshes on rejection (lines 15-21)
  • Uses React's use hook for suspense integration (line 26)
  • Provides a preload function for eager loading (line 33)

This pattern effectively enables synchronous inspection of the promise state, which is the core optimization for avoiding unnecessary suspense fallbacks.

integrations/react/src/__internal__/utils/SyncInspectablePromise.ts (6)

3-7: Well-designed interface for synchronous promise inspection.

The interface correctly extends Promise<T> with the additional fields needed for synchronous state inspection, which aligns with React's recommended pattern for optimizing use() hook rendering.


9-14: LGTM!

The as const pattern with dual const/type export is idiomatic TypeScript and provides good type safety.


16-27: LGTM!

Clean discriminated union type that enables proper type narrowing when handling promise states.


29-49: Good defensive validation in the inspect function.

The function correctly validates both the status field and the presence of value/reason fields before returning the typed state. The fallback error throw at line 47-48 handles any potential invalid state.


51-71: LGTM with a note on async status updates.

The .then() handlers update status asynchronously (microtask), meaning if an already-resolved native Promise is wrapped, the status won't reflect FULFILLED synchronously. This is acceptable for the intended use case (data loaders, dynamic imports) which are inherently async. The key optimization benefits come from preserving already-inspectable promises via liftValue's early return path.


73-89: Correct handling of all value types.

The function properly handles three cases:

  1. Existing SyncInspectablePromise instances are reused (preserving optimization).
  2. Plain PromiseLike objects are wrapped with makeSyncInspectable.
  3. Raw values are immediately wrapped as fulfilled promises with synchronously-available status.

The double type assertion at line 79 is a pragmatic choice given the runtime validation at lines 76-77.

integrations/react/src/future/loader/loaderPlugin.tsx (7)

8-13: LGTM!

Clean imports with proper use of type-only import for the interface.


23-26: LGTM!

The explicit SyncInspectablePromise<unknown> return type ensures consistent promise handling throughout the loader flow.


46-46: LGTM!

Using liftValue ensures initialLoaderData is normalized to a SyncInspectablePromise, enabling consistent handling regardless of whether the initial data is a raw value or a promise.


65-70: LGTM!

Good use of Promise.allSettled to ensure errors are logged without disrupting the flow.


98-101: LGTM!

Consistent type signature with the parent function.


132-137: LGTM!

The short-circuit evaluation correctly handles the optional loaderData, and inspect() provides synchronous status checks as intended for the suspense optimization.


140-153: LGTM!

Promise.allSettled handles the potentially undefined loaderData gracefully (treating it as an immediately-fulfilled value), and the .finally() ensures resume() is always called.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
.changeset/wild-houses-see.md (1)

1-5: Changeset looks correct; consider clarifying the breaking type change.

This is a valid Changesets entry and it’s good to see @stackflow/react is called out. The description is quite terse—consider explicitly naming the affected public type(s) and what changed (e.g., LazyActivityComponentType no longer being a React.LazyExoticComponent, and any _load/preload expectations). Also please double-check that major matches your SemVer/breaking-change intent for consumers.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bc9949c and 9306c3f.

📒 Files selected for processing (3)
  • .changeset/happy-ghosts-peel.md (1 hunks)
  • .changeset/wild-houses-see.md (1 hunks)
  • integrations/react/src/__internal__/utils/PreloadableLazyComponent.tsx (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • .changeset/happy-ghosts-peel.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • integrations/react/src/internal/utils/PreloadableLazyComponent.tsx
🧰 Additional context used
📓 Path-based instructions (1)
.changeset/*.md

📄 CodeRabbit inference engine (AGENTS.md)

Include a Changeset entry for any user-facing package change

Files:

  • .changeset/wild-houses-see.md
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Workers Builds: stackflow-docs
  • GitHub Check: Cloudflare Pages

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
integrations/react/package.json (1)

44-45: Consider aligning peerDependencies with react18-use requirements.

The react18-use package (as its name suggests) is designed for React 18, but peerDependencies currently allows react >=16.8.0. Users on React 16/17 may encounter runtime issues with the use hook polyfill. Consider updating the peer dependency to react >=18.0.0 or documenting that the /future export path requires React 18+.

integrations/react/src/__internal__/StructuredActivityComponentType.tsx (1)

92-110: Slight redundancy in liftValue calls, but functionally correct.

The implementation correctly handles both sync and async content resolution. However, liftValue on lines 100-102 wraps a synchronously created object, and liftValue on lines 105-109 wraps the chained promise. Both are necessary for the return type, but the pattern could be simplified by always using the contentPromise.then(...) path since liftValue handles already-fulfilled promises efficiently.

Consider simplifying to a single return path:

-    if (state.status === PromiseStatus.FULFILLED) {
-      return liftValue({
-        default: state.value.default.component,
-      });
-    }
-
-    return liftValue(
-      contentPromise.then((value) => ({
+    return liftValue(
+      contentPromise.then((value) => ({
         default: value.default.component,
       })),
     );
-    });

Since liftValue already optimizes for fulfilled promises internally, this achieves the same suspense optimization with less code.

integrations/react/src/__internal__/utils/SyncInspectablePromise.ts (1)

73-90: Consider optimizing the status check for better performance.

Lines 75-78 perform a heuristic check to detect if the value is already a SyncInspectablePromise. The check on line 78 uses Object.values(PromiseStatus).some(...), which iterates through all status values on every call.

For better performance, consider one of these alternatives:

Option 1: Direct comparison (most efficient)

     if (
       value instanceof Promise &&
       "status" in value &&
-      Object.values(PromiseStatus).some((status) => status === value.status)
+      (value.status === PromiseStatus.PENDING ||
+       value.status === PromiseStatus.FULFILLED ||
+       value.status === PromiseStatus.REJECTED)
     ) {

Option 2: Use a Set for O(1) lookup (if you prefer DRY)

const VALID_STATUSES = new Set(Object.values(PromiseStatus));

export function liftValue<T>(value: T): SyncInspectablePromise<Awaited<T>> {
  if (isPromiseLike(value)) {
    if (
      value instanceof Promise &&
      "status" in value &&
      VALID_STATUSES.has(value.status)
    ) {
      return value as SyncInspectablePromise<Awaited<T>>;
    }
    // ...
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d59f719 and b684f05.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (9)
  • .pnp.cjs (2 hunks)
  • integrations/react/package.json (1 hunks)
  • integrations/react/src/__internal__/StructuredActivityComponentType.tsx (3 hunks)
  • integrations/react/src/__internal__/utils/PreloadableLazyComponent.tsx (1 hunks)
  • integrations/react/src/__internal__/utils/SyncInspectablePromise.ts (1 hunks)
  • integrations/react/src/future/lazy.tsx (1 hunks)
  • integrations/react/src/future/loader/use.ts (0 hunks)
  • integrations/react/src/future/loader/useLoaderData.ts (1 hunks)
  • integrations/react/src/future/stackflow.tsx (4 hunks)
💤 Files with no reviewable changes (1)
  • integrations/react/src/future/loader/use.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • integrations/react/src/internal/utils/PreloadableLazyComponent.tsx
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Write source in TypeScript with strict typing enabled across the codebase

Files:

  • integrations/react/src/future/loader/useLoaderData.ts
  • integrations/react/src/future/lazy.tsx
  • integrations/react/src/future/stackflow.tsx
  • integrations/react/src/__internal__/utils/SyncInspectablePromise.ts
  • integrations/react/src/__internal__/StructuredActivityComponentType.tsx
integrations/react/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Use .tsx files for React components and JSX in the React integration package

Files:

  • integrations/react/src/future/lazy.tsx
  • integrations/react/src/future/stackflow.tsx
  • integrations/react/src/__internal__/StructuredActivityComponentType.tsx
🧬 Code graph analysis (3)
integrations/react/src/future/lazy.tsx (2)
integrations/react/src/__internal__/utils/PreloadableLazyComponent.tsx (1)
  • preloadableLazyComponent (9-35)
integrations/react/src/__internal__/utils/SyncInspectablePromise.ts (4)
  • liftValue (73-90)
  • inspect (29-49)
  • PromiseStatus (9-13)
  • PromiseStatus (14-14)
integrations/react/src/future/stackflow.tsx (1)
integrations/react/src/__internal__/utils/SyncInspectablePromise.ts (2)
  • SyncInspectablePromise (3-7)
  • liftValue (73-90)
integrations/react/src/__internal__/utils/SyncInspectablePromise.ts (1)
integrations/react/src/__internal__/utils/isPromiseLike.ts (1)
  • isPromiseLike (1-8)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Workers Builds: stackflow-docs
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (11)
.pnp.cjs (1)

6857-6857: The [email protected] dependency addition is valid and secure.

The .pnp.cjs changes are correctly auto-generated by Yarn based on the integrations/react/package.json update. Verification confirms:

  • Version constraint ^0.4.1 in package.json matches the entry in .pnp.cjs
  • Package [email protected] exists on npm with no known security vulnerabilities
  • Virtual path hashes are consistent across both hunks
integrations/react/src/future/stackflow.tsx (4)

23-26: LGTM!

Imports for liftValue and SyncInspectablePromise are correctly added to support the new promise handling pattern.


67-74: LGTM!

Type updates for loaderDataCacheMap and loadData correctly reflect the transition to SyncInspectablePromise<unknown> for synchronous promise inspection.


92-97: LGTM!

Wrapping the loader result with liftValue correctly converts it to a SyncInspectablePromise, enabling synchronous status inspection. This handles the case where loader is undefined (returning a fulfilled promise with undefined).


127-129: LGTM!

The simplified promise chain correctly handles cache invalidation. Since loaderData is a SyncInspectablePromise, rejection errors are captured in its status and reason properties for downstream inspection, making explicit re-throwing unnecessary here.

integrations/react/src/future/lazy.tsx (3)

3-8: LGTM!

Imports correctly bring in the utilities needed for the new preloadable lazy component pattern.


13-15: LGTM!

The factory function correctly wraps the dynamic import with liftValue to create a SyncInspectablePromise, enabling synchronous status inspection within preloadableLazyComponent.


17-30: LGTM!

The _load method correctly implements the suspense optimization pattern:

  • Returns a synchronously fulfilled promise when preload is complete, avoiding fallback exposure
  • Chains on the pending preload otherwise, maintaining correct async behavior

This aligns well with the PR objective of optimizing suspense fallback rendering.

integrations/react/src/__internal__/StructuredActivityComponentType.tsx (2)

5-12: LGTM!

Imports correctly updated to support the new preloadable lazy loading pattern with SyncInspectablePromise utilities.


36-55: LGTM!

Content caching logic correctly:

  • Invalidates cache on rejection to allow retry
  • Reuses pending/fulfilled promises to prevent duplicate loads
  • Uses liftValue to wrap the content promise for synchronous inspection
integrations/react/src/future/loader/useLoaderData.ts (1)

2-2: Switch to react18-use is compatible with your setup.

The react18-use version (^0.4.1) is correctly configured for React 18.3.1. The use() hook properly handles the SyncInspectablePromise (an internal stackflow utility) passed through activity context since it extends the standard Promise interface. Note: the status inspection capabilities (isResolved, isPending, isRejected) come from stackflow's internal SyncInspectablePromise utility, not from react18-use itself—react18-use provides the Promise suspension mechanism that React 18's use() hook needs.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
integrations/react/src/future/lazy.tsx (1)

18-34: Logic is sound; consider caching _load result for efficiency.

The _load implementation correctly handles all three states (fulfilled, rejected, pending) and properly delegates to the preloadable component mechanism. The flow is:

  • When preload is already fulfilled → immediate return
  • When preload is rejected → returns error via liftError
  • When preload is pending → returns promise that resolves after preload completes

However, each _load invocation creates a new liftValue(preload()) wrapper (line 22), even though the underlying component load is cached in preloadableLazyComponent. While functionally correct, this could be optimized by caching the first _load result.

Consider caching the _load result to avoid redundant promise wrapping:

 export function lazy<T extends { [K in keyof T]: any } = {}>(
   load: () => Promise<{ default: StaticActivityComponentType<T> }>,
 ): LazyActivityComponentType<T> {
   const { Component, preload } = preloadableLazyComponent(() =>
     liftValue(load()),
   );

+  let cachedLoadResult: ReturnType<LazyActivityComponentType<T>['_load']> | null = null;
+
   const LazyActivityComponent: LazyActivityComponentType<T> = Object.assign(
     Component,
     {
       _load: () => {
+        if (cachedLoadResult) {
+          return cachedLoadResult;
+        }
+
         const preloadTask = liftValue(preload());
         const preloadTaskState = inspect(preloadTask);

         if (preloadTaskState.status === PromiseStatus.FULFILLED) {
-          return liftValue({ default: Component });
+          cachedLoadResult = liftValue({ default: Component });
         } else if (preloadTaskState.status === PromiseStatus.REJECTED) {
           return liftError(preloadTaskState.reason);
+        } else {
+          cachedLoadResult = liftValue(preloadTask.then(() => ({ default: Component })));
         }

-        return liftValue(preloadTask.then(() => ({ default: Component })));
+        return cachedLoadResult;
       },
     },
   );

   return LazyActivityComponent;
 }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b684f05 and ebdf93a.

📒 Files selected for processing (2)
  • integrations/react/src/__internal__/utils/SyncInspectablePromise.ts (1 hunks)
  • integrations/react/src/future/lazy.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • integrations/react/src/internal/utils/SyncInspectablePromise.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Write source in TypeScript with strict typing enabled across the codebase

Files:

  • integrations/react/src/future/lazy.tsx
integrations/react/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Use .tsx files for React components and JSX in the React integration package

Files:

  • integrations/react/src/future/lazy.tsx
🧬 Code graph analysis (1)
integrations/react/src/future/lazy.tsx (4)
integrations/react/src/__internal__/StaticActivityComponentType.ts (1)
  • StaticActivityComponentType (3-4)
integrations/react/src/__internal__/LazyActivityComponentType.ts (1)
  • LazyActivityComponentType (3-6)
integrations/react/src/__internal__/utils/PreloadableLazyComponent.tsx (1)
  • preloadableLazyComponent (9-35)
integrations/react/src/__internal__/utils/SyncInspectablePromise.ts (5)
  • liftValue (73-90)
  • inspect (29-49)
  • PromiseStatus (9-13)
  • PromiseStatus (14-14)
  • liftError (92-97)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Workers Builds: stackflow-docs
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (2)
integrations/react/src/future/lazy.tsx (2)

3-9: LGTM! Clean integration with new preloadable utilities.

The imports correctly bring in the new preloadableLazyComponent utility and SyncInspectablePromise utilities (inspect, liftError, liftValue, PromiseStatus) that form the foundation of the refactored lazy loading implementation.


14-16: LGTM! Correct usage of preloadableLazyComponent.

The initialization properly wraps the user-provided load function with liftValue to create a SyncInspectablePromise, which aligns with the preloadableLazyComponent's expected signature. The lazy wrapper ensures load() is only invoked when needed.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
integrations/react/src/__internal__/StructuredActivityComponentType.tsx (1)

93-113: Implementation correctly enables the suspense optimization.

The promise state inspection allows synchronous resolution when content is already loaded, avoiding unnecessary Suspense fallbacks per the PR objective.

Minor readability suggestion: The final return block implicitly handles the PENDING state. Consider adding a comment or making this explicit for clarity:

    } else if (state.status === PromiseStatus.REJECTED) {
      return liftError(state.reason);
    }
 
+   // PENDING: content is still loading, return a promise that resolves to the component
    return liftValue(
      contentPromise.then((value) => ({
        default: value.default.component,
      })),
    );
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ebdf93a and ffc0904.

📒 Files selected for processing (1)
  • integrations/react/src/__internal__/StructuredActivityComponentType.tsx (3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Write source in TypeScript with strict typing enabled across the codebase

Files:

  • integrations/react/src/__internal__/StructuredActivityComponentType.tsx
integrations/react/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Use .tsx files for React components and JSX in the React integration package

Files:

  • integrations/react/src/__internal__/StructuredActivityComponentType.tsx
🧬 Code graph analysis (1)
integrations/react/src/__internal__/StructuredActivityComponentType.tsx (2)
integrations/react/src/__internal__/utils/SyncInspectablePromise.ts (6)
  • SyncInspectablePromise (3-7)
  • inspect (29-49)
  • PromiseStatus (9-13)
  • PromiseStatus (14-14)
  • liftValue (73-90)
  • liftError (92-97)
integrations/react/src/__internal__/utils/PreloadableLazyComponent.tsx (1)
  • preloadableLazyComponent (9-35)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Workers Builds: stackflow-docs
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (2)
integrations/react/src/__internal__/StructuredActivityComponentType.tsx (2)

5-13: LGTM!

The import changes properly reflect the migration from React.lazy to the new preloadableLazyComponent approach with SyncInspectablePromise utilities. Using type-only imports for React types is a good practice.


37-56: LGTM!

The caching logic correctly mirrors the pattern used in preloadableLazyComponent. The implementation properly:

  • Caches the promise per structured activity component instance
  • Refreshes on rejection to enable retry behavior
  • Uses liftValue to ensure the promise is synchronously inspectable

Copy link
Collaborator

@orionmiz orionmiz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both motivation and implementation make senses.

My only suggestion is to choose familiar names for promise utility functions.

@ENvironmentSet ENvironmentSet force-pushed the sync-promise-for-suspense-opt branch from c747cf7 to b7360c6 Compare December 15, 2025 13:30
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
integrations/react/src/__internal__/utils/SyncInspectablePromise.ts (2)

3-7: Type safety consideration: Optional fields without discriminated union.

The interface allows value and reason to be undefined regardless of status, which could lead to unsafe access. However, the inspect() function properly returns a discriminated union (PromiseState<T>) that enforces the relationship between status and field presence, mitigating this concern for external consumers.

Consider documenting that raw SyncInspectablePromise should only be accessed via inspect() for type-safe state queries.


73-90: Consider strengthening SyncInspectablePromise detection with a unique marker.

Lines 75-79 rely on duck-typing to detect existing SyncInspectablePromise instances by checking for a status property with matching enum values. While this works in practice since native Promises don't have a status property by default, it could theoretically misidentify any Promise that someone manually assigns a status property with a value matching PromiseStatus enum values, potentially causing inspect() to throw "Invalid promise state" if value or reason properties are missing.

Adding an explicit marker would make detection more explicit and robust:

export interface SyncInspectablePromise<T> extends Promise<T> {
  __isSyncInspectable: true;
  status: PromiseStatus;
  value?: T;
  reason?: unknown;
}

// In resolve function
if (
  value instanceof Promise &&
  "__isSyncInspectable" in value &&
  value.__isSyncInspectable === true
) {
  return value as SyncInspectablePromise<Awaited<T>>;
}
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ffc0904 and b7360c6.

📒 Files selected for processing (11)
  • .changeset/happy-ghosts-peel.md (1 hunks)
  • integrations/react/src/__internal__/LazyActivityComponentType.ts (1 hunks)
  • integrations/react/src/__internal__/StructuredActivityComponentType.tsx (3 hunks)
  • integrations/react/src/__internal__/utils/PreloadableLazyComponent.tsx (1 hunks)
  • integrations/react/src/__internal__/utils/SyncInspectablePromise.ts (1 hunks)
  • integrations/react/src/__internal__/utils/useThenable.ts (1 hunks)
  • integrations/react/src/future/lazy.tsx (1 hunks)
  • integrations/react/src/future/loader/loaderPlugin.tsx (6 hunks)
  • integrations/react/src/future/loader/use.ts (0 hunks)
  • integrations/react/src/future/loader/useLoaderData.ts (1 hunks)
  • integrations/react/src/future/stackflow.tsx (0 hunks)
💤 Files with no reviewable changes (2)
  • integrations/react/src/future/stackflow.tsx
  • integrations/react/src/future/loader/use.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • .changeset/happy-ghosts-peel.md
  • integrations/react/src/future/loader/useLoaderData.ts
  • integrations/react/src/internal/utils/PreloadableLazyComponent.tsx
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Write source in TypeScript with strict typing enabled across the codebase

Files:

  • integrations/react/src/__internal__/StructuredActivityComponentType.tsx
  • integrations/react/src/future/lazy.tsx
  • integrations/react/src/future/loader/loaderPlugin.tsx
  • integrations/react/src/__internal__/utils/SyncInspectablePromise.ts
  • integrations/react/src/__internal__/LazyActivityComponentType.ts
  • integrations/react/src/__internal__/utils/useThenable.ts
integrations/react/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Use .tsx files for React components and JSX in the React integration package

Files:

  • integrations/react/src/__internal__/StructuredActivityComponentType.tsx
  • integrations/react/src/future/lazy.tsx
  • integrations/react/src/future/loader/loaderPlugin.tsx
🧬 Code graph analysis (6)
integrations/react/src/__internal__/StructuredActivityComponentType.tsx (3)
integrations/react/src/__internal__/utils/SyncInspectablePromise.ts (6)
  • SyncInspectablePromise (3-7)
  • inspect (29-49)
  • PromiseStatus (9-13)
  • PromiseStatus (14-14)
  • resolve (73-90)
  • reject (92-97)
config/src/InferActivityParams.ts (1)
  • InferActivityParams (5-8)
integrations/react/src/__internal__/utils/PreloadableLazyComponent.tsx (1)
  • preloadableLazyComponent (9-35)
integrations/react/src/future/lazy.tsx (4)
integrations/react/src/__internal__/StaticActivityComponentType.ts (1)
  • StaticActivityComponentType (3-4)
integrations/react/src/__internal__/LazyActivityComponentType.ts (1)
  • LazyActivityComponentType (3-6)
integrations/react/src/__internal__/utils/PreloadableLazyComponent.tsx (1)
  • preloadableLazyComponent (9-35)
integrations/react/src/__internal__/utils/SyncInspectablePromise.ts (5)
  • resolve (73-90)
  • inspect (29-49)
  • PromiseStatus (9-13)
  • PromiseStatus (14-14)
  • reject (92-97)
integrations/react/src/future/loader/loaderPlugin.tsx (1)
integrations/react/src/__internal__/utils/SyncInspectablePromise.ts (5)
  • SyncInspectablePromise (3-7)
  • resolve (73-90)
  • inspect (29-49)
  • PromiseStatus (9-13)
  • PromiseStatus (14-14)
integrations/react/src/__internal__/utils/SyncInspectablePromise.ts (1)
integrations/react/src/__internal__/utils/isPromiseLike.ts (1)
  • isPromiseLike (1-8)
integrations/react/src/__internal__/LazyActivityComponentType.ts (1)
integrations/react/src/__internal__/StaticActivityComponentType.ts (1)
  • StaticActivityComponentType (3-4)
integrations/react/src/__internal__/utils/useThenable.ts (1)
integrations/react/src/__internal__/utils/SyncInspectablePromise.ts (4)
  • resolve (73-90)
  • inspect (29-49)
  • PromiseStatus (9-13)
  • PromiseStatus (14-14)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Workers Builds: stackflow-docs
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (12)
integrations/react/src/__internal__/utils/useThenable.ts (1)

3-14: LGTM! Clean suspense integration.

The implementation correctly follows React's suspense pattern by:

  • Converting the thenable to a synchronously inspectable promise
  • Returning the value when fulfilled
  • Throwing the error when rejected
  • Throwing the promise when pending to trigger suspense boundaries
integrations/react/src/__internal__/utils/SyncInspectablePromise.ts (3)

29-49: LGTM! Proper discriminated union handling.

The inspect() function correctly returns a type-safe discriminated union by checking both the status and property existence before accessing value or reason. The defensive error throw for invalid states is good practice.


51-71: Promise mutation pattern is correct but unconventional.

The function creates a promise and mutates it by attaching status, value, and reason properties through then handlers. This is the core mechanism enabling synchronous inspection and is working as intended. The promise returned at line 59 is intentionally not stored since we only need the side effect of updating the promise state.


92-97: LGTM! Straightforward rejected promise creation.

The function correctly creates a rejected promise and immediately marks it with the appropriate status and reason, ensuring synchronous inspectability.

integrations/react/src/__internal__/StructuredActivityComponentType.tsx (3)

1-13: LGTM! Imports correctly support the new preloadable architecture.

The imports include the necessary utilities for synchronously inspectable promises and preloadable lazy components.


37-56: LGTM! Proper caching with error recovery.

The implementation correctly caches the content loading promise and automatically retries on rejection, which provides good error recovery behavior. The use of resolve() ensures the promise is synchronously inspectable.


93-113: LGTM! Comprehensive state handling in content component loading.

The rewrite correctly handles all three promise states:

  • FULFILLED: Immediately returns the resolved component
  • REJECTED: Propagates the error
  • PENDING: Returns a promise that will resolve to the component when ready

The use of preloadableLazyComponent integrates seamlessly with the new synchronously inspectable promise system.

integrations/react/src/future/lazy.tsx (1)

11-37: LGTM! Proper integration with preloadable lazy components.

The rewrite correctly:

  1. Uses preloadableLazyComponent to create a component that leverages cached loading
  2. Implements _load method that returns synchronously inspectable promises
  3. Handles all three states (fulfilled, rejected, pending) appropriately
  4. Ensures Component is ready to render without suspending after preload completes
integrations/react/src/future/loader/loaderPlugin.tsx (4)

41-49: LGTM! Consistent SyncInspectablePromise wrapping.

Wrapping initialLoaderData with resolve() ensures that the loader data is always a synchronously inspectable promise, maintaining consistency with the new architecture.


63-70: LGTM! Simplified and consistent error handling.

The change to always treat loaderData as a SyncInspectablePromise eliminates the need for runtime type checking with isPromiseLike, resulting in cleaner and more predictable code.


120-128: LGTM! Robust lazy component promise resolution with defensive checks.

The implementation correctly handles multiple component types (structured with function content, structured with direct content, lazy with _load, and static). The added type check for typeof matchActivityComponent._load === "function" on line 125 provides good defensive programming.


132-154: LGTM! Key optimization using synchronous promise inspection.

The updated pending state check using inspect() is the core optimization of this PR. It allows checking promise status synchronously, avoiding unnecessary pauses and suspense fallback rendering when promises are already settled. The Promise.allSettled pattern correctly handles both loader data and lazy component promises, including the case where loaderData is undefined.

@ENvironmentSet ENvironmentSet enabled auto-merge (squash) December 16, 2025 07:00
@ENvironmentSet ENvironmentSet merged commit 3cb6e33 into main Dec 16, 2025
6 of 9 checks passed
@ENvironmentSet ENvironmentSet deleted the sync-promise-for-suspense-opt branch December 16, 2025 07:00
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.

3 participants