Skip to content

Conversation

@ENvironmentSet
Copy link
Collaborator

@ENvironmentSet ENvironmentSet commented Jul 25, 2025

This PR introduces default history setup options and structured activity components to enable richer deep link experiences in Stackflow applications.

1. Structured Activity Components

  • New StructuredActivityComponentType<P> interface that separates concerns:
    • content: Core component logic (required)
    • layout?: Wrapper for common UI patterns (optional)
    • loading?: Loading state component (optional)
    • error?: Error boundary component (optional)

2. Default History Feature

  • New defaultHistory route option: Pre-seeds navigation stack for better deep link UX
  • HistoryEntry type: Strongly typed history entries with activity names and parameters

@changeset-bot
Copy link

changeset-bot bot commented Jul 25, 2025

🦋 Changeset detected

Latest commit: da7bcd1

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

This PR includes changesets to release 3 packages
Name Type
@stackflow/react Minor
@stackflow/plugin-preload Patch
@stackflow/plugin-history-sync 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

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Jul 25, 2025

Deploying stackflow-demo with  Cloudflare Pages  Cloudflare Pages

Latest commit: 4711c2e
Status: ✅  Deploy successful!
Preview URL: https://528d3abb.stackflow-demo.pages.dev
Branch Preview URL: https://deep-link-default-nav-contex.stackflow-demo.pages.dev

View logs

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jul 25, 2025

@stackflow/demo

yarn add https://pkg.pr.new/@stackflow/[email protected]
yarn add https://pkg.pr.new/@stackflow/[email protected]
yarn add https://pkg.pr.new/@stackflow/[email protected]

commit: da7bcd1

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Jul 25, 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 da7bcd1 Commit Preview URL Sep 05 2025, 07:21 AM

@coderabbitai
Copy link

coderabbitai bot commented Aug 11, 2025

Warning

Rate limit exceeded

@ENvironmentSet has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 8 minutes and 58 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between d65e0c6 and da7bcd1.

📒 Files selected for processing (4)
  • integrations/react/src/__internal__/StructuredActivityComponentType.tsx (1 hunks)
  • integrations/react/src/future/usePrepare.ts (2 hunks)
  • integrations/react/src/stable/index.ts (1 hunks)
  • integrations/react/src/stable/stackflow.tsx (4 hunks)
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Introduces Structured Activity Components to compose content, layout, loading, and error states.
    • Adds a defaultHistory route option in the history-sync plugin to pre-seed navigation for better deep-link experiences.
    • Demo updated: Article rebuilt with structured components, dedicated layout and loading screen; new loading spinner.
  • Improvements

    • More reliable initialization/sequencing of default history and smoother loading/prefetching for structured content.
  • Chores

    • Release metadata updated to align with new type constraints.

Walkthrough

Adds a StructuredActivityComponent system and exposes it via the React integration; introduces a route-level defaultHistory option and queue-driven insertion in the history-sync plugin; refactors demo Article into structured pieces, adds a loading spinner, and adjusts loader and type mappings across integrations.

Changes

Cohort / File(s) Summary
Release metadata
\.changeset/chubby-pianos-heal.md, \.changeset/new-pandas-return.md, \.changeset/gold-groups-own.md
Adds changesets: minor for @stackflow/react (StructuredActivityComponentType), minor for @stackflow/plugin-history-sync (defaultHistory), patch for @stackflow/plugin-preload (type sync).
Demo — Article structured migration
demo/src/activities/Article.tsx (removed), demo/src/activities/Article/* (Article.tsx, Article.content.tsx, Article.layout.tsx, Article.loading.tsx, Article.loading.css.ts, Article.content.css.ts, Article.loader.ts)
Replaces monolithic Article with structured activity: adds structured Article (content/layout/loading), lazy content import, module augmentation; removes old flat Article; adds 5s loader delay and related CSS.
Demo — Spinner and imports
demo/src/components/LoadingSpinner.css.ts, demo/src/components/LoadingSpinner.tsx, demo/src/activities/Main/Main.css.ts, demo/src/activities/Main/Main.tsx
Adds LoadingSpinner (vanilla-extract + React) and updates Main import paths.
Demo — Stack config/entrypoints
demo/src/stackflow/Stack.tsx, demo/src/stackflow/stackflow.config.ts, demo/src/stackflow/stackflow.docs.ts
Updates imports to named/direct Article and Main paths; adds defaultHistory function to Article route to pre-seed history; adjusts loader import paths and docs imports.
History sync — types
extensions/plugin-history-sync/src/RouteLike.ts
Adds HistoryEntry type; extends Route with defaultHistory?(params); broadens RouteLike to accept Route or Route[].
History sync — plugin behavior
extensions/plugin-history-sync/src/historySyncPlugin.tsx
Implements default-history queue, checkpointing, and insertion tasks; updates lifecycle hooks (onInit, adds onChanged, onPopped, onStepPopped); refines routing, path guards, and push/step sequencing; updates generic signature.
React integrations — structured API & types
integrations/react/src/__internal__/StructuredActivityComponentType.tsx, integrations/react/src/__internal__/MonolithicActivityComponentType.ts, integrations/react/src/__internal__/StaticActivityComponentType.ts, integrations/react/src/__internal__/LazyActivityComponentType.ts, integrations/react/src/__internal__/ActivityComponentType.ts, integrations/react/src/future/ActivityComponentType.tsx, integrations/react/src/future/index.ts, integrations/react/src/future/stackflow.tsx
Introduces StructuredActivityComponentType (branding, helpers, content/layout/loading/errorHandler), adds MonolithicActivityComponentType, loosens generic constraints, updates ActivityComponentType wiring, re-exports structured API, and ties type mappings to inferred activity params.
React integrations — rendering & loader changes
integrations/react/src/__internal__/PluginRenderer.tsx, integrations/react/src/future/loader/loaderPlugin.tsx, integrations/react/src/future/usePrepare.ts, integrations/react/src/stable/stackflow.tsx, integrations/react/src/__internal__/MainRenderer.tsx, integrations/react/src/future/lazy.tsx
Renderer now detects structured activities and composes Content/Layout/Loading/Error via Suspense and an error boundary; loader prefers structured content() when resolving lazy content and updates pause/resume logic; usePrepare prefetches content(); some type/memoization adjustments and internal renames.
Preload plugin — type constraint
extensions/plugin-preload/src/pluginPreload.tsx
Narrows loader mapping inference to activities whose params are string
Stable/public API type adjustments
integrations/react/src/stable/index.ts, integrations/react/src/stable/BaseActivities.ts, integrations/react/src/stable/useActions.ts
Re-exports and public-type surfaces adjusted to use MonolithicActivityComponentType in place of the previous ActivityComponentType mapping; signatures updated accordingly.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant App as App
  participant Stack as Stackflow (React)
  participant Renderer as PluginRenderer
  participant SAC as StructuredActivityComponent
  participant Loader as Loader (future)
  participant React as React (Suspense/ErrorBoundary)

  App->>Stack: navigate to Activity
  Stack->>Renderer: render Activity
  alt Monolithic component
    Renderer->>Renderer: render <Activity params />
  else Structured activity
    Renderer->>SAC: detect content/layout/loading/error
    par Prefetch resolution
      Stack->>Loader: before-route: resolve content()/_load
      Loader-->>Stack: pause until ready (if needed)
    end
    Renderer->>React: wrap with Suspense (Loading)
    Renderer->>React: wrap with ErrorBoundary (ErrorHandler)
    Renderer->>SAC: get Content component
    Renderer-->>App: render <Layout?><Content/><Layout?>
  end
Loading
sequenceDiagram
  autonumber
  participant Browser as Browser URL
  participant Plugin as historySyncPlugin
  participant Actions as Stackflow Actions
  participant Queue as DefaultHistory Queue
  participant Stack as Stack

  Browser->>Plugin: onInit(currentPath)
  Plugin->>Plugin: resolve route + params
  Plugin->>Plugin: derive defaultHistory[]
  alt has defaultHistory
    Plugin->>Actions: dispatch initial Pushed (first entry, backdated)
    Plugin->>Queue: enqueue pushes for remaining entries/steps
  end
  note over Plugin,Queue: Checkpoint when stack idle triggers flush
  Plugin->>Actions: onChanged() -> flush queued tasks
  Actions->>Stack: push/stepPush per queued entries
  rect rgb(247,247,255)
  note right of Plugin: Non-default push/replace/pop clears queue
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch deep-link-default-nav-context

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@ENvironmentSet ENvironmentSet changed the title feat(plugin-history-sync): Default history setup option for rich deep link experiences feat(react, plugin-history-sync): Default history setup option for rich deep link experiences Aug 14, 2025
@ENvironmentSet ENvironmentSet marked this pull request as ready for review August 14, 2025 09:25
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: 5

🔭 Outside diff range comments (1)
extensions/plugin-history-sync/src/historySyncPlugin.tsx (1)

161-166: Hash path extraction can return empty string; use a safer fallback

When the URL has no hash, location.hash.split("#")[1] yields an empty string, not undefined, so the current code returns "". Prefer stripping the leading # and fallback to "/".

-          if (options.useHash) {
-            return location.hash.split("#")[1] ?? "/";
-          }
+          if (options.useHash) {
+            const hashed = location.hash.replace(/^#/, "");
+            return hashed || "/";
+          }
🧹 Nitpick comments (10)
.changeset/new-pandas-return.md (1)

5-5: Polish wording for clarity and consistency (“deep‑link”, option name).

More explicit and grammatically tighter phrasing helps when reading release notes.

-Default history setup option for rich deep link experiences
+Add defaultHistory route option to pre-seed stack for deep‑link experiences
.changeset/chubby-pianos-heal.md (1)

5-5: Fix grammar in changeset description.

Use active/imperative voice for release notes.

-Make lazy activity components can be forced to render
+Allow lazy activity components to be forced to render
demo/src/activities/Article.placeholder.css.ts (1)

3-8: Consider adding width to ensure consistent centering across layouts.

If the parent container doesn’t stretch horizontally by default, adding width improves robustness.

 export const container = style({
   display: "flex",
   alignItems: "center",
   justifyContent: "center",
+  width: "100%",
   height: "100%",
 });
integrations/react/src/__internal__/LazyActivityComponentType.ts (1)

2-7: Type shape LGTM; simplify generic constraint for readability.

The current constraint is effectively “object-like.” Using object communicates intent more clearly without changing behavior.

-export type LazyActivityComponentType<T extends { [K in keyof T]: any } = {}> =
+export type LazyActivityComponentType<T extends object = {}> =
   ComponentType<{ params: T; shouldRenderImmediately?: boolean }> & {
     _load?: () => Promise<{ default: StaticActivityComponentType<T> }>;
   };
demo/src/components/LoadingSpinner.tsx (1)

5-7: Add ARIA semantics to the spinner for accessibility

Expose the loading state to assistive technologies. A minimal enhancement is to mark it as a status region and provide an accessible label.

Apply this diff:

-const LoadingSpinner: FC = () => {
-  return <div className={css.spinner} />;
-};
+const LoadingSpinner: FC<{ ariaLabel?: string }> = ({ ariaLabel = "Loading" }) => {
+  return (
+    <div
+      className={css.spinner}
+      role="status"
+      aria-live="polite"
+      aria-label={ariaLabel}
+    />
+  );
+};
demo/src/components/LoadingSpinner.css.ts (1)

11-14: Prefer design tokens over hardcoded colors for consistency and theming

Replace hex colors with Seed Design tokens to align with the rest of the demo and enable theme coherence.

Apply this diff in the style block:

-  border: "4px solid #ccc",
-  borderTop: "4px solid #333",
+  border: `4px solid ${vars.$semantic.color.divider3}`,
+  borderTop: `4px solid ${vars.$scale.color.gray900}`,

And add the token import:

// at the top of the file
import { vars } from "@seed-design/design-token";
integrations/react/src/__internal__/PluginRenderer.tsx (1)

40-49: Minor readability refactor: cache lazy context before branch

Functionally correct. Caching the computed context removes repetition and clarifies the conditional.

Apply this diff:

-              let output: React.ReactNode =
-                "_load" in Activity &&
-                typeof (activity.context as any)?.lazyActivityComponentRenderContext === "object" ? (
-                  <Activity
-                    {...(activity.context as any)?.lazyActivityComponentRenderContext}
-                    params={activity.params}
-                  />
-                ) : (
-                  <Activity params={activity.params} />
-                );
+              const lazyCtx = (activity.context as any)?.lazyActivityComponentRenderContext;
+              let output: React.ReactNode =
+                "_load" in Activity && typeof lazyCtx === "object" ? (
+                  <Activity {...lazyCtx} params={activity.params} />
+                ) : (
+                  <Activity params={activity.params} />
+                );
extensions/plugin-history-sync/src/historySyncPlugin.tsx (3)

186-197: Prefer using makeTemplate.parse for params instead of manual UrlPattern parsing

You parse pathParams via a new UrlPattern instead of the already-constructed template, then merge with search params. This duplicates logic and can diverge from decode behavior (template.parse applies route.decode). If defaultHistory expects decoded values, this may be incorrect.

  • If defaultHistory should receive decoded params: replace this block by using template.parse(currentPath) and fall back to search params only when null.
  • If defaultHistory should receive raw string params: keep as-is, but consider adding a code comment clarifying the expectation to avoid regressions.

Suggested change (decoded-params version):

-        const pattern = new UrlPattern(
-          `${targetActivityRoute.path}/`,
-          options.urlPatternOptions,
-        );
-        const url = pathToUrl(currentPath);
-        const pathParams = pattern.match(url.pathname);
-        const searchParams = urlSearchParamsToMap(url.searchParams);
-        const params = {
-          ...searchParams,
-          ...pathParams,
-        };
+        const template = makeTemplate(
+          targetActivityRoute,
+          options.urlPatternOptions,
+        );
+        const parsed = template.parse(currentPath);
+        const params =
+          parsed ??
+          urlSearchParamsToMap(pathToUrl(currentPath).searchParams);

286-301: Typo: enoughtPastTime → enoughPastTime

Minor readability nit.

-          const enoughtPastTime = new Date().getTime() - MINUTE;
+          const enoughPastTime = new Date().getTime() - MINUTE;
...
-              eventDate: enoughtPastTime,
+              eventDate: enoughPastTime,

613-631: Guard readability: prefer negative checks over === false

The path-injection guard is correct, but readability suffers from using in with === false. Consider a more idiomatic check.

-        if (
-          !actionParams.activityContext ||
-          "path" in actionParams.activityContext === false
-        ) {
+        if (
+          !actionParams.activityContext ||
+          !("path" in actionParams.activityContext)
+        ) {

Same comment applies to onBeforeReplace.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

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

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6dbc319 and e6b4ce9.

📒 Files selected for processing (15)
  • .changeset/chubby-pianos-heal.md (1 hunks)
  • .changeset/new-pandas-return.md (1 hunks)
  • demo/src/activities/Article.placeholder.css.ts (1 hunks)
  • demo/src/activities/Article.placeholder.tsx (1 hunks)
  • demo/src/components/LoadingSpinner.css.ts (1 hunks)
  • demo/src/components/LoadingSpinner.tsx (1 hunks)
  • demo/src/stackflow/Stack.tsx (1 hunks)
  • demo/src/stackflow/stackflow.config.ts (1 hunks)
  • extensions/plugin-history-sync/src/RouteLike.ts (1 hunks)
  • extensions/plugin-history-sync/src/historySyncPlugin.tsx (14 hunks)
  • integrations/react/src/__internal__/LazyActivityComponentType.ts (1 hunks)
  • integrations/react/src/__internal__/PluginRenderer.tsx (1 hunks)
  • integrations/react/src/future/lazy.ts (0 hunks)
  • integrations/react/src/future/lazy.tsx (1 hunks)
  • integrations/react/src/future/loader/loaderPlugin.tsx (1 hunks)
💤 Files with no reviewable changes (1)
  • integrations/react/src/future/lazy.ts
🧰 Additional context used
🧬 Code Graph Analysis (6)
integrations/react/src/__internal__/PluginRenderer.tsx (1)
core/src/Stack.ts (1)
  • Activity (28-43)
integrations/react/src/future/lazy.tsx (2)
integrations/react/src/__internal__/StaticActivityComponentType.ts (1)
  • StaticActivityComponentType (3-5)
integrations/react/src/__internal__/LazyActivityComponentType.ts (1)
  • LazyActivityComponentType (4-7)
integrations/react/src/future/loader/loaderPlugin.tsx (1)
core/src/utils/makeActions.ts (2)
  • pause (100-113)
  • resume (114-127)
demo/src/stackflow/Stack.tsx (1)
integrations/react/src/future/lazy.tsx (1)
  • lazy (9-43)
extensions/plugin-history-sync/src/RouteLike.ts (2)
config/src/RegisteredActivityName.ts (1)
  • RegisteredActivityName (3-6)
config/src/RegisteredActivityParamTypes.ts (1)
  • RegisteredActivityParamTypes (4-8)
extensions/plugin-history-sync/src/historySyncPlugin.tsx (4)
core/src/interfaces/StackflowActions.ts (1)
  • StackflowActions (15-65)
extensions/plugin-history-sync/src/makeHistoryTaskQueue.ts (1)
  • makeHistoryTaskQueue (7-30)
extensions/plugin-history-sync/src/makeTemplate.ts (3)
  • makeTemplate (48-110)
  • pathToUrl (5-7)
  • urlSearchParamsToMap (9-17)
core/src/event-utils/makeEvent.ts (1)
  • makeEvent (5-19)
🪛 LanguageTool
.changeset/new-pandas-return.md

[style] ~5-~5: Consider a different adjective to strengthen your wording.
Context: ... Default history setup option for rich deep link experiences

(DEEP_PROFOUND)

⏰ 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 (7)
extensions/plugin-history-sync/src/RouteLike.ts (1)

15-24: Confirm additionalSteps shape matches the core step model.

Typing additionalSteps to the same activity K’s params is great if steps are constrained to the same activity. If steps can reference different activities, this will be too narrow and may need a discriminated union including activityName per step.

If steps can vary by activity, a possible alternative would be:

  • Change additionalSteps to an array of HistoryEntry-like step items.
  • Or define a dedicated HistoryStep union mirroring the runtime shape.
demo/src/stackflow/Stack.tsx (2)

14-17: LGTM: Placeholder integration with the new lazy API is correct

Using buildPlaceholder with the Article placeholder component aligns with the new lazy loader contract and should provide a smooth deep-link loading experience.


36-39: No action needed — loader gating and Suspense coverage verified

Quick summary: loaderPlugin pauses navigation when a loader or lazy component is pending unless the activity context requests immediate rendering; historySyncPlugin sets shouldRenderImmediately for history-driven initial entries; PluginRenderer forwards that context to lazy activities; demo also wraps in a Suspense boundary.

Key locations:

  • integrations/react/src/future/loader/loaderPlugin.tsx — pauses (gates) when loader/lazy promises exist unless activity.context.lazyActivityComponentRenderContext?.shouldRenderImmediately === true.
  • integrations/react/src/future/lazy.tsx — wraps the lazy component in Suspense only when a placeholder exists AND shouldRenderImmediately is true; otherwise it renders the lazy component (relies on gating or an outer Suspense).
  • integrations/react/src/internal/PluginRenderer.tsx — forwards lazyActivityComponentRenderContext into the Activity props when a component is lazy.
  • integrations/react/src/future/stackflow.tsx — loaderPlugin is appended after user plugins (commented/ensured ordering).
  • extensions/plugin-history-sync/src/historySyncPlugin.tsx — sets lazyActivityComponentRenderContext.shouldRenderImmediately = true for initial/history-based pushes.
  • demo/src/stackflow/Stack.tsx — uses historySyncPlugin(...)
  • demo/src/App.tsx — wraps in a Suspense boundary.

Conclusion: the codebase already provides both gating (loaderPlugin) and Suspense coverage (historySyncPlugin + demo-level Suspense) to avoid "suspended without fallback".

demo/src/activities/Article.placeholder.tsx (1)

7-17: LGTM: Placeholder comp correctly uses params and composes the spinner

Structure and usage of useActivityParams and LoadingSpinner look good.

demo/src/stackflow/stackflow.config.ts (1)

20-25: defaultHistory wiring looks correct; verify interplay with initialActivity and types

The defaultHistory seeding to Main aligns with the new history plugin behavior. Two quick checks:

  • Ensure the route’s defaultHistory function signature matches the plugin type (params are strings per UrlPattern parsing). You’re not using params, which is fine.
  • With history-sync providing initial events, initialActivity may be effectively bypassed for deep links. Confirm the intended precedence and that you won’t end up with duplicated initial pushes in non-deeplink cases.

Would you like me to scan the repo to ensure all defaultHistory route definitions match the expected signature and to check the plugin registration order relative to initialActivity handling?

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

30-39: Render lazy component within a Suspense boundary even without a placeholder

If there’s no placeholder or shouldRenderImmediately is false, you render the lazy component without Suspense. That relies on an upstream Suspense boundary; without it, React will throw at runtime. If a global Suspense boundary is not guaranteed, wrap with Suspense and use a minimal fallback.

Do you guarantee a Suspense boundary upstream for all lazy activity render paths (e.g., PluginRenderer)? If not, consider this change:

-    if (placeholder && shouldRenderImmediately) {
-      return (
-        <Suspense fallback={placeholder}>
-          <LazyLoadedActivityComponent params={params} />
-        </Suspense>
-      );
-    }
-
-    return <LazyLoadedActivityComponent params={params} />;
+    const shouldShowPlaceholder = placeholder && shouldRenderImmediately;
+    return (
+      <Suspense fallback={shouldShowPlaceholder ? placeholder : null}>
+        <LazyLoadedActivityComponent params={params} />
+      </Suspense>
+    );
extensions/plugin-history-sync/src/historySyncPlugin.tsx (1)

725-727: onChanged hook is wired in core — no change needed

Confirmed: the core exposes and invokes the onChanged post-effect hook and your plugin’s onChanged will be called after state changes.

  • core/src/interfaces/StackflowPlugin.ts — onChanged is declared on the plugin interface.
  • core/src/produceEffects.ts — emits a "%SOMETHING_CHANGED%" post-effect when state output changes.
  • core/src/utils/triggerPostEffectHooks.ts — dispatches plugin.onChanged?.({ actions, effect }) for that effect.
  • extensions/plugin-history-sync/src/historySyncPlugin.tsx — your onChanged handler (defaultHistorySetupCheckpoint(actions)) is in the right place and will be invoked.

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 (4)
integrations/react/src/future/lazy.tsx (4)

32-45: Always use the placeholder as Suspense fallback when provided

Currently, the placeholder is used as a fallback only when shouldRenderImmediately is true. If a placeholder is provided, it’s generally useful as a Suspense fallback in all cases. This also simplifies the branching.

If this matches the intended UX, consider:

-		if (placeholder && shouldRenderImmediately) {
-			if (transitionState === "enter-active") {
-				return placeholder;
-			}
-
-			return (
-				<Suspense fallback={placeholder}>
-					<LazyLoadedActivityComponent params={params} />
-				</Suspense>
-			);
-		}
-
-		return <LazyLoadedActivityComponent params={params} />;
+		if (placeholder) {
+			if (shouldRenderImmediately && transitionState === "enter-active") {
+				return placeholder;
+			}
+			return (
+				<Suspense fallback={placeholder}>
+					<LazyLoadedActivityComponent params={params} />
+				</Suspense>
+			);
+		}
+		return <LazyLoadedActivityComponent params={params} />;

If you intentionally want to rely on an upstream Suspense boundary when shouldRenderImmediately is false, feel free to ignore this.


46-48: Set a displayName for better DevTools traces

Small DX improvement: set a displayName so the wrapper shows up clearly in React DevTools and error stacks.

 	LazyActivityComponent._load = cachedLoad;
 
+	LazyActivityComponent.displayName = "LazyActivityComponent";

10-13: Tighten the generic constraint to avoid any

Prefer unknown over any for safer typing. If your internal types expect indexable params, this will still work while preventing accidental unsafe access.

-export function lazy<T extends { [K in keyof T]: any } = {}>(
+export function lazy<T extends Record<string, unknown> = Record<string, never>>(

Note: If StaticActivityComponentType<T> depends on { [K in keyof T]: any } specifically, you may need to adjust the companion type to accept unknown.


25-31: Consider passing params to the placeholder builder (future enhancement)

Placeholders often benefit from route params (e.g., sizing, skeleton variants). Exposing them is a small ergonomic win and keeps placeholders in sync with the activity’s params.

Proposed API:

  • Change config type:
    • buildPlaceholder?: (params: T) => ReactNode
  • And call as:
    • const placeholder = config?.buildPlaceholder?.(params);

If you’d like, I can open a follow-up PR with the type and call-site updates across the demo and internals.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

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

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b73d25a and f975084.

📒 Files selected for processing (1)
  • integrations/react/src/future/lazy.tsx (1 hunks)
⏰ 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). (5)
  • GitHub Check: Build all packages
  • GitHub Check: Create PR or release packages
  • GitHub Check: Check whether the written test passes normally
  • GitHub Check: Check the TypeScript typings
  • GitHub Check: Workers Builds: stackflow-docs
🔇 Additional comments (2)
integrations/react/src/future/lazy.tsx (2)

46-46: Confirm _load typing matches LazyActivityComponentType

Ensure LazyActivityComponentType<T> declares _load: () => Promise<{ default: StaticActivityComponentType<T> }> (or the LoadedModule alias). This prevents property-assignment errors and improves API discoverability for callers who want to preload.


10-10: API shape and overall direction look solid

Generics, cached dynamic import, and the wrapper with shouldRenderImmediately are coherent and align with the PR’s goals.

@ENvironmentSet
Copy link
Collaborator Author

@coderabbitai perform full review

@coderabbitai
Copy link

coderabbitai bot commented Aug 19, 2025

@ENvironmentSet I'll perform a comprehensive review of this PR which introduces default history setup options and structured activity components for enhanced deep link experiences.

This is a substantial feature addition that touches multiple core areas of the Stackflow framework. The changes include:

  1. New Structured Activity Components: Introduction of StructuredActivityComponentType that allows separation of concerns (content, layout, loading, error handling)
  2. Default History Feature: New defaultHistory route option to pre-seed navigation stack
  3. Type System Improvements: Enhanced type constraints and better inference
  4. Demo Application Refactoring: Migration to structured component architecture

Key areas I'll focus on during the review:

  • Architecture & Design: Evaluate the structured component approach and default history implementation
  • Type Safety: Review the enhanced type system and constraints
  • Implementation Quality: Check for proper error handling, performance considerations, and code organization
  • Breaking Changes: Assess impact on existing APIs
  • Testing: Identify areas that may need additional test coverage

The feature appears well-architected with clear separation of concerns and should significantly improve the deep linking experience in Stackflow applications.

✅ Actions performed

Full review triggered.

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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
integrations/react/src/stable/stackflow.tsx (1)

311-319: Bug: replace() generates two different activityIds when none is provided.

makeActivityId() is called twice; the ID used for the action may differ from the one you return. Use the computed activityId for both.

Apply this diff:

       replace(activityName, activityParams, options) {
-        const activityId = options?.activityId ?? makeActivityId();
+        const activityId = options?.activityId ?? makeActivityId();

         getCoreStore()?.actions.replace({
-          activityId: options?.activityId ?? makeActivityId(),
+          activityId,
           activityName,
           activityParams,
           skipEnterActiveState: parseActionOptions(options).skipActiveState,
         });

         return {
           activityId,
         };
       },
♻️ Duplicate comments (7)
.changeset/chubby-pianos-heal.md (1)

7-8: Fix grammar and capitalization in the public changeset entry

Tighten the phrasing and use the correct brand capitalization.

-Structured activity components are components modeling activity view while exposing major features of an activity.
-Allowing developers easily customize user experience of an activity view, it even allow stackflow to perform various kinds of optimizations.
+Structured activity components model an activity view while exposing its major features.
+They allow developers to easily customize the user experience of an activity view and enable Stackflow to perform various optimizations.
extensions/plugin-history-sync/src/RouteLike.ts (2)

26-30: Follow through the generic rename in RouteLike.

Keep the naming consistent to avoid confusion with React’s ComponentType.

-export type RouteLike<ComponentType> =
+export type RouteLike<TComponent> =
   | string
   | string[]
-  | Route<ComponentType>
-  | Route<ComponentType>[];
+  | Route<TComponent>
+  | Route<TComponent>[];

7-13: Make defaultHistory receive decoded params; also rename the generic for clarity.

This keeps defaultHistory aligned with decode() and avoids forcing consumers to handle raw strings when typed params are available.

Apply this diff:

-export type Route<ComponentType> = {
+export type Route<TComponent> = {
   path: string;
   decode?: (
     params: Record<string, string>,
-  ) => ComponentType extends ActivityComponentType<infer U> ? U : {};
-  defaultHistory?: (params: Record<string, string>) => HistoryEntry[];
+  ) => TComponent extends ActivityComponentType<infer U> ? U : {};
+  defaultHistory?: (
+    params: TComponent extends ActivityComponentType<infer U> ? U : {},
+  ) => HistoryEntry[];
 };

And update the union below accordingly (see next comment).

After updating the types, ensure the plugin calls defaultHistory with decoded params (or parsed fallback). If needed, I can propose the corresponding changes in historySyncPlugin.tsx.

demo/src/stackflow/Stack.tsx (1)

5-15: Confirm Article component shape (structured vs monolithic) and prop contracts

You’re now passing Article directly in the components map. If Article is a structured activity object, ensure its loading/layout components accept the required params shape; if it’s monolithic, previous concerns are resolved.

If Article is structured, and ArticleLoading/ArticleLayout don’t accept params, wrap them or add a params prop. Example adapter:

-    Article,
+    Article: {
+      content: Article.content,
+      loading: ({ params }) => <ArticleLoading />,
+      layout: ({ params, children }) => <ArticleLayout>{children}</ArticleLayout>,
+      // error: ...
+    },

Also note: removing lazy-loading for Article trades off code-splitting for simplicity in the demo; that’s fine for the demo, just flagging the change in loading behavior.

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

121-129: Fix wait gating and always attach handlers to avoid unhandled rejections

Two issues:

  • Pausing logic: Using OR with the loading check forces pause even when a Loading UI exists. Use AND so we only block when we must.
  • Handlers: When shouldRenderImmediately === true and a Loading UI exists, you skip attaching handlers entirely, risking unhandled rejections and losing error logs.

Apply:

-    const lazyComponentPromise =
-      "_load" in contentComponent ? contentComponent._load?.() : undefined;
-    const shouldRenderImmediately = (activityContext as any)
-      ?.lazyActivityComponentRenderContext?.shouldRenderImmediately;
-
-    if (
-      (loaderDataPromise || lazyComponentPromise) &&
-      (shouldRenderImmediately !== true ||
-        "loading" in matchActivityComponent === false)
-    ) {
-      pause();
-
-      Promise.allSettled([loaderDataPromise, lazyComponentPromise])
-        .then(([loaderDataPromiseResult, lazyComponentPromiseResult]) => {
-          printLoaderDataPromiseError({
-            promiseResult: loaderDataPromiseResult,
-            activityName: matchActivity.name,
-          });
-          printLazyComponentPromiseError({
-            promiseResult: lazyComponentPromiseResult,
-            activityName: matchActivity.name,
-          });
-        })
-        .finally(() => {
-          resume();
-        });
-    }
+    const lazyComponentPromise =
+      "_load" in contentComponent ? contentComponent._load?.() : undefined;
+    const shouldRenderImmediately =
+      (activityContext as any)?.lazyActivityComponentRenderContext
+        ?.shouldRenderImmediately;
+
+    // Always attach handlers for error visibility, irrespective of waiting.
+    const settled = Promise.allSettled([
+      loaderDataPromise,
+      lazyComponentPromise,
+    ]).then(([loaderDataPromiseResult, lazyComponentPromiseResult]) => {
+      printLoaderDataPromiseError({
+        promiseResult: loaderDataPromiseResult,
+        activityName: matchActivity.name,
+      });
+      printLazyComponentPromiseError({
+        promiseResult: lazyComponentPromiseResult,
+        activityName: matchActivity.name,
+      });
+    });
+
+    // Only pause when we should wait AND there is no component-level Loading UI.
+    const shouldWait =
+      (loaderDataPromise || lazyComponentPromise) &&
+      shouldRenderImmediately !== true &&
+      ("loading" in matchActivityComponent === false);
+
+    if (shouldWait) {
+      pause();
+      settled.finally(() => {
+        resume();
+      });
+    }

Also applies to: 132-146

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

100-106: Remount ErrorBoundary on param changes to clear stale error state

Without a key tied to params, an error state can persist across param-only updates.

-      ErrorComponent ? (
-        <StructuredActivityComponentErrorBoundary
+      ErrorComponent ? (
+        <StructuredActivityComponentErrorBoundary
+          key={JSON.stringify(params)}
           renderFallback={(err, reset) => (
             <ErrorComponent params={params} error={err} reset={reset} />
           )}
         >
           {node}
         </StructuredActivityComponentErrorBoundary>
       ) : (
         node
       ),
extensions/plugin-history-sync/src/historySyncPlugin.tsx (1)

186-196: Fix brittle pattern and null-spread crash when building defaultHistory params

  • Appending a trailing slash to the UrlPattern is brittle; it can cause mismatches for paths that do not end with “/”.
  • UrlPattern.match() can return null. Spreading ...pathParams will throw in native object spread semantics.

Use the route path as-is and guard the match with a fallback to {}.

Apply this diff:

-        const pattern = new UrlPattern(
-          `${targetActivityRoute.path}/`,
-          options.urlPatternOptions,
-        );
+        const pattern = new UrlPattern(
+          targetActivityRoute.path,
+          options.urlPatternOptions,
+        );
         const url = pathToUrl(currentPath);
-        const pathParams = pattern.match(url.pathname);
+        const pathParams = pattern.match(url.pathname) ?? {};
         const searchParams = urlSearchParamsToMap(url.searchParams);
         const params = {
           ...searchParams,
           ...pathParams,
         };

Optional: You can also reuse the earlier makeTemplate(...).parse(currentPath) result to avoid a second matcher.

🧹 Nitpick comments (23)
.changeset/gold-groups-own.md (1)

5-5: Polish the description for consistency and clarity

Consider tightening the language and using backticks for the package name.

-Sync with type constraint changes in stackflow/react
+Sync with type-constraint changes in `@stackflow/react`.
demo/src/activities/Article/Article.loading.css.ts (1)

3-9: Prefer viewport-safe vertical centering (mobile-safe)

height: "100%" centers only if a parent provides explicit height. To ensure vertical centering across pages and mobile browsers (iOS dynamic viewport), add a viewport-constrained minHeight.

 export const container = style({
   display: "flex",
   alignItems: "center",
   justifyContent: "center",
-  height: "100%",
+  height: "100%",
+  // Ensure full-viewport centering even when no parent height is set
+  minHeight: "100dvh",
   width: "100%",
 });
demo/src/components/LoadingSpinner.css.ts (1)

15-15: Respect prefers-reduced-motion for accessibility

Disable or reduce animation for users who prefer reduced motion.

 export const spinner = style({
   width: 48,
   height: 48,
   border: `4px solid ${vars.$scale.color.gray600}`,
   borderTop: `4px solid ${vars.$scale.color.gray900}`,
   borderRadius: "50%",
-  animation: `${spin} 1s linear infinite`,
+  animation: `${spin} 1s linear infinite`,
+  "@media": {
+    "(prefers-reduced-motion: reduce)": {
+      animation: "none",
+    },
+  },
 });
demo/src/components/LoadingSpinner.tsx (1)

5-7: Add basic ARIA for screen readers and allow className passthrough

Expose a label and className prop, and add role/aria-live for assistive tech. Keeps defaults simple while enabling composition.

-const LoadingSpinner: FC = () => {
-  return <div className={css.spinner} />;
-};
+const LoadingSpinner: FC<{ label?: string; className?: string }> = ({
+  label = "Loading…",
+  className,
+}) => {
+  return (
+    <div
+      role="status"
+      aria-live="polite"
+      aria-label={label}
+      className={[css.spinner, className].filter(Boolean).join(" ")}
+    />
+  );
+};
integrations/react/src/stable/stackflow.tsx (1)

262-262: Mutation of activityComponentMap won’t trigger context updates post-mount (by design).

This is fine since addActivity() is disallowed after <Stack /> is rendered. If this policy ever changes, remember that mutating the same object reference won’t propagate via context and you’d need to replace the map object to trigger updates.

integrations/react/src/__internal__/StaticActivityComponentType.ts (1)

3-4: Prefer a clearer object constraint for params.

T extends {} is permissive but a bit opaque. Consider a more explicit constraint for readability and intent.

-export type StaticActivityComponentType<T extends {} = {}> =
-  React.ComponentType<{ params: T }>;
+export type StaticActivityComponentType<T extends Record<string, unknown> = {}> =
+  React.ComponentType<{ params: T }>;
extensions/plugin-preload/src/pluginPreload.tsx (1)

14-15: Avoid self-referential infer U extends { [key in keyof U]: ... } constraint; use a safer two-step check.

Self-referential constraints can be brittle and sometimes lead to hard-to-debug inference issues. A safer pattern is to infer U first, then narrow with a conditional.

Apply this diff:

-    [key in Extract<keyof T, string>]?: T[key] extends ActivityComponentType<
-      infer U extends { [key in keyof U]: string | undefined }
-    >
-      ? Loader<U>
-      : Loader<{}>;
+    [key in Extract<keyof T, string>]?: T[key] extends ActivityComponentType<infer U>
+      ? U extends Record<string, string | undefined>
+        ? Loader<U>
+        : Loader<{}>
+      : Loader<{}>;

Please run your local typecheck to ensure this narrows as intended across monolithic and structured activity types.

.changeset/new-pandas-return.md (1)

5-5: Tighten changeset copy (grammar and clarity)

Use a definite article and hyphenate “deep‑link” for clarity.

-Add `defaultHistory` route option to pre-seed stack for better deep link experiences
+Add a `defaultHistory` route option to pre-seed the navigation stack for richer deep‑link experiences
demo/src/activities/Article/Article.content.css.ts (1)

4-4: Consider path aliases to reduce brittle relative imports

As these files move deeper, ../../ chains can get noisy and fragile. Consider setting up a TS path alias (e.g., @demo/styles) and updating the bundler to match.

If you want, I can propose a tsconfig and Vite/Next config patch.

demo/src/activities/Article/Article.loading.tsx (1)

5-11: Minor a11y check for loading UI.

If LoadingSpinner doesn’t already expose appropriate ARIA (e.g., role="status" and an accessible label), consider adding it at the container or spinner to ensure screen readers announce progress.

integrations/react/src/future/usePrepare.ts (3)

37-41: Guard against undefined content in misconfigured maps.

If a structured entry has content defined but set to undefined at runtime (misconfiguration), the current code will blow up when checking _load. A small nullish fallback makes this more robust without changing semantics.

Apply:

-    const contentComponent =
-      "content" in activityComponentMap[activityName]
-        ? activityComponentMap[activityName].content
-        : activityComponentMap[activityName];
+    const contentComponent =
+      "content" in activityComponentMap[activityName] &&
+      activityComponentMap[activityName].content
+        ? activityComponentMap[activityName].content
+        : activityComponentMap[activityName];

42-46: Tighten lazy prefetch guard; avoid queuing a resolved undefined.

Using "in" checks truthiness of the property, but you still call ._load?.() which can enqueue a resolved undefined. Prefer verifying it’s a function before invoking.

Apply:

-    if ("_load" in contentComponent) {
-      const lazyComponent = contentComponent;
-
-      prefetchTasks.push(Promise.resolve(lazyComponent._load?.()));
-    }
+    if (
+      contentComponent &&
+      typeof (contentComponent as any)._load === "function"
+    ) {
+      prefetchTasks.push((contentComponent as any)._load());
+    }

48-49: Consider swallowing prefetch errors to not block navigation.

If a loader or lazy prefetch fails, prepare will reject. Depending on UX, you may want to log and continue, letting runtime rendering handle errors.

Option:

-    await Promise.all(prefetchTasks);
+    // Prefer not to block navigation on prefetch failures
+    await Promise.allSettled(prefetchTasks);

If the intended contract is to fail fast, ignore this.

integrations/react/src/__internal__/LazyActivityComponentType.ts (1)

3-6: Generic bound T extends {} admits primitives; consider object.

If T represents props, allowing primitives (e.g., string) via T extends {} is likely unintended. Using object (or a record) tightens the constraint while keeping inference friendly.

Minimal change:

-export type LazyActivityComponentType<T extends {} = {}> =
+export type LazyActivityComponentType<T extends object = {}> =

If other types in this PR have standardised on {}, keeping it for consistency is fine; just ensure no call sites rely on primitives for T.

demo/src/activities/Article/Article.tsx (1)

14-18: Consider adding an error component for resiliency.

Since the structured type supports it, wiring an error boundary component (e.g., ArticleError) will improve UX for loader/content failures.

If you have (or want) an error component:

 export const Article: ActivityComponentType<"Article"> = {
   content: lazy(() => import("./Article.content")),
   layout: ArticleLayout,
   loading: ArticleLoading,
+  // Optional but recommended
+  // error: ArticleError,
 };
integrations/react/src/future/ActivityLoadingComponentType.ts (1)

1-9: Align public “future” types on React type exposure policy

This type directly imports React’s ComponentType, while ActivityComponentType was refactored to avoid exposing React types at the public surface via an internal alias. Consider aligning on a single approach (either consistently expose React types or consistently hide them behind internal aliases) to avoid API inconsistency and reduce future churn.

Would you like me to draft a small internal alias (e.g., an internal LoadingComponentType) and update the public type to reference it?

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

4-9: Tighten generic constraint and verify content type choice

  • Prefer P extends Record<string, unknown> over P extends {} for clearer intent and better soundness.
  • Double-check that content: MonolithicActivityComponentType

    is intentional. If content is meant to be a “content-only” component (as opposed to a full monolithic activity), consider introducing/using a dedicated ActivityContentComponentType to avoid over-permissive props at the type level.

Apply this small constraint improvement:

-export interface StructuredActivityComponentType<P extends {}> {
+export interface StructuredActivityComponentType<P extends Record<string, unknown>> {
integrations/react/src/future/stackflow.tsx (1)

36-45: Make StackflowInput components generic match the stricter function generic

StackflowInput currently uses ActivityComponentType, while the stackflow() function’s R generic is already tighter with ActivityComponentType<InferActivityParams>. Aligning StackflowInput improves type safety when the type is used standalone.

Apply:

 export type StackflowInput<
   T extends ActivityDefinition<RegisteredActivityName>,
   R extends {
-    [activityName in RegisteredActivityName]: ActivityComponentType<any>;
+    [activityName in RegisteredActivityName]: ActivityComponentType<
+      InferActivityParams<activityName>
+    >;
   },
 > = {
   config: Config<T>;
   components: R;
   plugins?: Array<StackflowPluginsEntry>;
 };
demo/src/activities/Article/Article.content.tsx (2)

16-21: Add alt attribute to image for accessibility

The hero image lacks an alt attribute. Use the article title or an empty alt if decorative.

           <LazyLoadImage
             src={imageUrl}
             effect="opacity"
             width="100%"
             height="100%"
+            alt={title}
           />

36-36: Remove stray trailing space in section title

Minor polish.

-        <div className={css.sectionTitle}>Other Items by Emila </div>
+        <div className={css.sectionTitle}>Other Items by Emila</div>
integrations/react/src/__internal__/PluginRenderer.tsx (1)

38-43: Type consistency nit: prefer ReactNode (already imported) over React.ReactNode

Keeps style consistent within the file.

-              let output: React.ReactNode =
+              let output: ReactNode =
                 "content" in Activity ? (
                   renderStructuredActivityComponent(Activity, activity.params)
                 ) : (
                   <Activity params={activity.params} />
                 );
extensions/plugin-history-sync/src/historySyncPlugin.tsx (2)

98-114: Avoid lingering entity set by clearing it when the queue drains

When the last pending task is shifted and the queue becomes null, also clear defaultHistoryEntryEntities. This prevents unnecessary memory retention and avoids any future false positives if more default-history work is ever scheduled.

Apply this diff:

       const nextTask = pendingDefaultHistoryEntryInsertionTasks.shift();

       if (pendingDefaultHistoryEntryInsertionTasks.length === 0) {
         pendingDefaultHistoryEntryInsertionTasks = null;
+        defaultHistoryEntryEntities.clear();
       }

       if (nextTask) {
         nextTask(actions);
       }

616-635: Simplify guard for path injection into activityContext

The current check uses the in operator; prefer a straightforward nullish check for readability and to avoid prototype-chain quirks.

Apply these diffs:

-        if (
-          !actionParams.activityContext ||
-          "path" in actionParams.activityContext === false
-        ) {
+        if (!actionParams.activityContext?.path) {
           const match = activityRoutes.find(
             (r) => r.activityName === actionParams.activityName,
           )!;
           const template = makeTemplate(match, options.urlPatternOptions);
           const path = template.fill(actionParams.activityParams);

           overrideActionParams({
             ...actionParams,
             activityContext: {
               ...actionParams.activityContext,
               path,
             },
           });
         }
-        if (
-          !actionParams.activityContext ||
-          "path" in actionParams.activityContext === false
-        ) {
+        if (!actionParams.activityContext?.path) {
           const match = activityRoutes.find(
             (r) => r.activityName === actionParams.activityName,
           )!;
           const template = makeTemplate(match, options.urlPatternOptions);
           const path = template.fill(actionParams.activityParams);

           overrideActionParams({
             ...actionParams,
             activityContext: {
               ...actionParams.activityContext,
               path,
             },
           });
         }

Also applies to: 640-657

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • 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 79ef981 and f85871e.

📒 Files selected for processing (38)
  • .changeset/chubby-pianos-heal.md (1 hunks)
  • .changeset/gold-groups-own.md (1 hunks)
  • .changeset/new-pandas-return.md (1 hunks)
  • demo/src/activities/Article.tsx (0 hunks)
  • demo/src/activities/Article/Article.content.css.ts (1 hunks)
  • demo/src/activities/Article/Article.content.tsx (1 hunks)
  • demo/src/activities/Article/Article.layout.tsx (1 hunks)
  • demo/src/activities/Article/Article.loading.css.ts (1 hunks)
  • demo/src/activities/Article/Article.loading.tsx (1 hunks)
  • demo/src/activities/Article/Article.tsx (1 hunks)
  • demo/src/activities/Main/Main.css.ts (1 hunks)
  • demo/src/activities/Main/Main.tsx (1 hunks)
  • demo/src/components/LoadingSpinner.css.ts (1 hunks)
  • demo/src/components/LoadingSpinner.tsx (1 hunks)
  • demo/src/stackflow/Stack.tsx (1 hunks)
  • demo/src/stackflow/stackflow.config.ts (2 hunks)
  • demo/src/stackflow/stackflow.docs.ts (1 hunks)
  • extensions/plugin-history-sync/src/RouteLike.ts (1 hunks)
  • extensions/plugin-history-sync/src/historySyncPlugin.tsx (13 hunks)
  • extensions/plugin-preload/src/pluginPreload.tsx (1 hunks)
  • integrations/react/src/__internal__/ActivityComponentType.ts (1 hunks)
  • integrations/react/src/__internal__/LazyActivityComponentType.ts (1 hunks)
  • integrations/react/src/__internal__/MainRenderer.tsx (0 hunks)
  • integrations/react/src/__internal__/MonolithicActivityComponentType.ts (1 hunks)
  • integrations/react/src/__internal__/PluginRenderer.tsx (3 hunks)
  • integrations/react/src/__internal__/StaticActivityComponentType.ts (1 hunks)
  • integrations/react/src/__internal__/StructuredActivityComponentType.tsx (1 hunks)
  • integrations/react/src/future/ActivityComponentType.tsx (1 hunks)
  • integrations/react/src/future/ActivityContentComponentType.ts (1 hunks)
  • integrations/react/src/future/ActivityErrorComponentType.ts (1 hunks)
  • integrations/react/src/future/ActivityLayoutComponentType.ts (1 hunks)
  • integrations/react/src/future/ActivityLoadingComponentType.ts (1 hunks)
  • integrations/react/src/future/index.ts (1 hunks)
  • integrations/react/src/future/lazy.tsx (1 hunks)
  • integrations/react/src/future/loader/loaderPlugin.tsx (2 hunks)
  • integrations/react/src/future/stackflow.tsx (2 hunks)
  • integrations/react/src/future/usePrepare.ts (1 hunks)
  • integrations/react/src/stable/stackflow.tsx (2 hunks)
💤 Files with no reviewable changes (2)
  • integrations/react/src/internal/MainRenderer.tsx
  • demo/src/activities/Article.tsx
🧰 Additional context used
🪛 LanguageTool
.changeset/chubby-pianos-heal.md

[grammar] ~8-~8: There might be a mistake here.
Context: ...an activity. Allowing developers easily customize user experience of an activity view, it...

(QB_NEW_EN)


[grammar] ~8-~8: There might be a mistake here.
Context: ... perform various kinds of optimizations.

(QB_NEW_EN)

.changeset/new-pandas-return.md

[grammar] ~5-~5: There might be a mistake here.
Context: ...dd defaultHistory route option to pre-seed stack for better deep link experiences ...

(QB_NEW_EN)


[style] ~5-~5: Consider a different adjective to strengthen your wording.
Context: ...ute option to pre-seed stack for better deep link experiences

(DEEP_PROFOUND)

⏰ 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). (1)
  • GitHub Check: Workers Builds: stackflow-docs
🔇 Additional comments (30)
demo/src/components/LoadingSpinner.css.ts (1)

9-16: LGTM on size typing; numeric lengths are valid in vanilla-extract

Using numeric values for width/height is correct here (vanilla-extract appends px for length properties). The animation and color tokens are also appropriate.

demo/src/components/LoadingSpinner.tsx (1)

1-9: Component structure is clean and minimal

The zero-dependency implementation and default export are straightforward and easy to consume.

integrations/react/src/stable/stackflow.tsx (1)

138-141: Dropping memo() around activity components: verify render/perf implications and structured-type compatibility.

Storing raw components (or structured entries) directly in activityComponentMap aligns with the new flexible component shapes. However, removing memo() may increase re-renders for monolithic components when used by consumers that rely on referential stability.

  • Please confirm that consumers don’t rely on memoized activity components for perf.
  • Ensure MainRenderer (and any consumers) treat these as relatively stable and don’t recreate elements in render loops unnecessarily.

If helpful, I can draft a lightweight micro-benchmark or profiling recipe to compare before/after perf characteristics in the demo app.

extensions/plugin-history-sync/src/RouteLike.ts (1)

15-24: Confirm step params shape for additionalSteps.

additionalSteps.stepParams is typed as RegisteredActivityParamTypes[K]. If step params have a distinct registered type (often they do), consider introducing a dedicated RegisteredStepParamTypes mapping instead.

If steps are guaranteed to mirror their activity’s params, this is fine; otherwise, please factor a separate step params registry.

demo/src/activities/Main/Main.css.ts (1)

3-3: Unable to locate export f in the styles module

I searched demo/src/styles/** for any export const f or export { f } declarations and found none. Please verify where f is defined and update this import (and any others) accordingly.

• File to update: demo/src/activities/Main/Main.css.ts (line 3)
• Ensure the target module exports a named f (or adjust the path to the file that does)

.changeset/new-pandas-return.md (1)

1-3: Changeset metadata looks correct

Package and bump type are valid for introducing a new route option.

demo/src/activities/Article/Article.content.css.ts (1)

4-4: Import path update LGTM

Path change to the centralized styles barrel is consistent with the repo’s restructuring. All usages of f.* in this file remain valid.

demo/src/activities/Main/Main.tsx (1)

4-9: Import path realignment is consistent and safe

Only path relocations; no behavioral changes. This should tree-shake identically and keeps the component wiring intact.

demo/src/stackflow/stackflow.docs.ts (1)

5-6: Export shapes confirmed
Article is correctly exported as a named export and Main remains a default export—no further changes needed.

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

23-28: Rename to LazyActivityComponent is clear and consistent

The naming aligns with the surrounding types and keeps _load attached for prefetch. No behavior change introduced.

demo/src/activities/Article/Article.loading.tsx (1)

3-3: Import alias validated – no changes needed

I’ve confirmed that demo/src/activities/Article/Article.loading.css.ts exists and that importing it via ./Article.loading.css is correct under the project’s vanilla-extract setup.

integrations/react/src/future/ActivityErrorComponentType.ts (1)

7-13: Type looks solid and aligned with error boundary patterns.

Good use of unknown for error and explicit reset callback. No issues.

integrations/react/src/__internal__/MonolithicActivityComponentType.ts (1)

4-6: Clear consolidation of static and lazy component types.

This alias simplifies downstream typing and pairs well with the structured component shape introduced elsewhere. Looks good.

integrations/react/src/future/index.ts (1)

6-9: Re-exports look good and clarify the public future API surface.

Adding these typed component re-exports improves DX and keeps the surface cohesive.

demo/src/activities/Article/Article.tsx (2)

5-12: Module augmentation matches route decode shape.

Param types for Article are well-defined (articleId: number; title?: string) and align with the stack config’s decode.


15-15: Default export shape verified for ArticleContent

I’ve confirmed that:

  • demo/src/activities/Article/Article.content.tsx exports ArticleContent as its default.
  • ArticleContent is a React functional component and thus satisfies the ComponentType<{ params: … }> requirement of ActivityContentComponentType<"Article">.

No further changes are needed here.

demo/src/stackflow/stackflow.config.ts (2)

2-3: Loaders correctly exported – no changes needed.

Verified that

  • demo/src/activities/Article/Article.loader.ts exports async function articleLoader
  • demo/src/activities/Main/Main.loader.ts exports function mainLoader

No further action required.


20-25: defaultHistory only applies to the Article route—no duplicate Main
I’ve checked extensions/plugin-history-sync/src/historySyncPlugin.tsx and confirmed that it calls targetActivityRoute.defaultHistory (which is only defined on the Article route in demo/src/stackflow/stackflow.config.ts). Since the Main route has no defaultHistory, starting at initialActivity: "Main" won’t reinsert another Main entry. No changes needed here.

integrations/react/src/future/ActivityContentComponentType.ts (1)

1-11: Type alias is correct and consistent with the structured activity pattern.

Generic constraints and params inference look good.

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

5-8: LGTM: Public type now correctly delegates to internal alias

Good move to decouple the public API from React types by routing through the internal alias while preserving per-activity param inference.

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

58-61: LGTM: Per-activity param inference on components map

Using ActivityComponentType<InferActivityParams> tightens the public typing and prevents accidental param mismatches per activity.

demo/src/activities/Article/Article.layout.tsx (1)

5-12: LGTM: clean layout wrapper with correct typing

Reads the title via useActivityParams and passes it to Layout; ReactNode typing is correct. DisplayName set appropriately.

integrations/react/src/__internal__/ActivityComponentType.ts (1)

4-6: LGTM: unified activity component alias covers monolithic and structured variants

The union alias is clearer and keeps call sites decoupled from implementation specifics.

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

105-108: Content-component resolution is correct

Deferring to structuredActivityComponent.content when available aligns the loader with the structured model.

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

91-111: Wrapper order is correct (Layout(ErrorBoundary(Suspense(Content))))

This ordering properly catches lazy-load errors and shows loading fallbacks. Nice.

extensions/plugin-history-sync/src/historySyncPlugin.tsx (5)

286-305: Good fix: pre-register initial activity id for default history sequencing

Registering initialActivityId in defaultHistoryEntryEntities and reusing it for the initial Pushed event resolves the sequencing bug where the initial push cleared pending default-history tasks.


312-315: LGTM: query params fallback for routes without defaultHistory

Using template.parse(currentPath) ?? urlSearchParamsToMap(...) avoids null-spread and preserves query params when path variables don’t match. Consistent with the intended deep-link behavior.


488-495: LGTM: cancel default-history queue on non-default push

Clearing pending tasks when a pushed activity id isn’t part of the default-history set prevents stale preseed tasks from polluting user-driven navigation.


520-525: LGTM: cancel default-history queue on non-default step push

Same reasoning as activities; protects the stack from unintended step insertions once the user interacts.


606-615: LGTM: cleanup on pop events

Clearing pending default-history tasks on onPopped/onStepPopped aligns with user-driven back navigation and avoids inconsistent stack state.

anakin_karrot added 3 commits August 20, 2025 11:50
@ENvironmentSet
Copy link
Collaborator Author

@CodeRabbit write me a detailed PR summary

@coderabbitai
Copy link

coderabbitai bot commented Aug 20, 2025

Tip

For best results, initiate chat on the files or code changes.

@ENvironmentSet Here's a detailed summary of PR #610:

🎯 Overview

This PR introduces default history setup options and structured activity components to enable richer deep link experiences in Stackflow applications. It's a substantial architectural enhancement that modernizes the component system while maintaining backward compatibility.

🏗️ Major Architectural Changes

1. Structured Activity Components

  • New StructuredActivityComponentType<P> interface that separates concerns:

    • content: Core component logic (required)
    • layout?: Wrapper for common UI patterns (optional)
    • loading?: Loading state component (optional)
    • error?: Error boundary component (optional)
  • Enhanced Plugin Renderer now supports both traditional function components and structured components

  • New Error Boundary: StructuredActivityComponentErrorBoundary for graceful error handling

  • Suspense Integration: Automatic loading state management with custom loading components

2. Default History Feature

  • New defaultHistory route option: Pre-seeds navigation stack for better deep link UX
  • HistoryEntry type: Strongly typed history entries with activity names and parameters
  • Enhanced Plugin: plugin-history-sync now supports history initialization with checkpoint-based processing
  • Queue Management: Sophisticated task queuing system for default history entry insertion

🔧 Type System Improvements

Enhanced Type Constraints

  • Relaxed generic constraints from T extends { [K in keyof T]: any } to T extends {}
  • Better Activity Parameter Inference: ActivityComponentType<InferActivityParams<ActivityName>>
  • Specialized Component Types:
    • ActivityContentComponentType<ActivityName>
    • ActivityLayoutComponentType<ActivityName>
    • ActivityLoadingComponentType<ActivityName>
    • ActivityErrorComponentType<ActivityName>

Improved Type Safety

  • Plugin Preload: Enhanced loader mapping with stricter type inference
  • Route Configuration: Better type safety for defaultHistory functions
  • Component Resolution: Content-aware lazy loading with proper type propagation

📦 Package-Specific Changes

@stackflow/react (Minor)

  • New StructuredActivityComponentType export
  • Enhanced ActivityComponentType union (now includes structured components)
  • Improved loader plugin with content component resolution
  • Better lazy component handling in usePrepare

@stackflow/plugin-history-sync (Minor)

  • New defaultHistory route option
  • HistoryEntry type for typed history management
  • Enhanced plugin with lifecycle hooks (onChanged, onPopped, onStepPopped)
  • Robust checkpoint-based history initialization

@stackflow/plugin-preload (Patch)

  • Type constraint synchronization with react package changes

🎨 Demo Application Transformation

Restructured Architecture

  • Article Activity: Migrated to structured component pattern
    • Article.content.tsx: Core article rendering logic
    • Article.layout.tsx: Layout wrapper with title handling
    • Article.loading.tsx: Custom loading component with spinner
    • Article.tsx: Configuration using structured component pattern

Enhanced User Experience

  • 5-second loader delay: Demonstrates loading states effectively
  • Default History Setup: Article route pre-seeds with Main activity
  • Improved File Organization: Activities now use dedicated directories
  • New LoadingSpinner Component: Reusable loading indicator

Path Updates

  • Updated import paths throughout demo for new directory structure
  • Consistent relative path usage (../../ patterns)

🔄 Migration & Compatibility

Non-Breaking Changes

  • Existing ActivityComponentType usage remains valid
  • Traditional function components continue to work unchanged
  • Lazy loading behavior preserved for existing implementations

Opt-in Enhancements

  • defaultHistory is optional - routes work without it
  • Structured components are optional - use when needed
  • Enhanced type inference improves developer experience without breaking existing code

🎯 Key Benefits

  1. Better Deep Links: Default history pre-seeding creates more natural navigation flows
  2. Separation of Concerns: Structured components enable cleaner architecture
  3. Enhanced DX: Better TypeScript inference and error messages
  4. Performance: Optimized loading strategies with content-aware lazy loading
  5. Maintainability: Clearer component organization and error boundaries

📊 Impact Assessment

  • Risk: Low - changes are additive and backward compatible
  • Complexity: Medium-High - new concepts but well-documented through demo
  • Adoption: Gradual - teams can migrate incrementally
  • Performance: Positive - better loading strategies and error handling

This PR significantly enhances Stackflow's capabilities for building rich, navigable applications while maintaining the framework's ease of use and type safety.

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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
integrations/react/src/future/loader/loaderPlugin.tsx (1)

105-112: Guard the "in" operator: potential runtime TypeError when component is missing

You’re using "content" in matchActivityComponent before verifying that matchActivityComponent exists. If the component isn’t registered, this throws at runtime. Move the null-check before the "in" check.

Apply this diff to reorder the guard:

-  const matchActivityComponent = input.components[activityName as T["name"]];
-  const contentComponent =
-    "content" in matchActivityComponent
-      ? matchActivityComponent.content
-      : matchActivityComponent;
-
-  if (!matchActivity || !matchActivityComponent) {
-    return;
-  }
+  const matchActivityComponent = input.components[activityName as T["name"]];
+
+  if (!matchActivity || !matchActivityComponent) {
+    return;
+  }
+
+  const contentComponent =
+    "content" in matchActivityComponent
+      ? matchActivityComponent.content
+      : matchActivityComponent;
♻️ Duplicate comments (2)
integrations/react/src/future/loader/loaderPlugin.tsx (1)

125-145: Unhandled rejections when shouldRenderImmediately === true; attach handlers regardless of waiting

When shouldRenderImmediately is true, no handlers are attached to loaderDataPromise/lazyComponentPromise. Rejections will surface as unhandled and you lose error logging. Always attach allSettled handlers; only pause/resume when you actually need to wait.

Apply this diff to always attach handlers while preserving non-blocking behavior:

-    if (
-      (loaderDataPromise || lazyComponentPromise) &&
-      shouldRenderImmediately !== true
-    ) {
-      pause();
-
-      Promise.allSettled([loaderDataPromise, lazyComponentPromise])
-        .then(([loaderDataPromiseResult, lazyComponentPromiseResult]) => {
-          printLoaderDataPromiseError({
-            promiseResult: loaderDataPromiseResult,
-            activityName: matchActivity.name,
-          });
-          printLazyComponentPromiseError({
-            promiseResult: lazyComponentPromiseResult,
-            activityName: matchActivity.name,
-          });
-        })
-        .finally(() => {
-          resume();
-        });
-    }
+    if (loaderDataPromise || lazyComponentPromise) {
+      const shouldWait = shouldRenderImmediately !== true;
+      const settled = Promise.allSettled([
+        loaderDataPromise,
+        lazyComponentPromise,
+      ]).then(([loaderDataPromiseResult, lazyComponentPromiseResult]) => {
+        printLoaderDataPromiseError({
+          promiseResult: loaderDataPromiseResult,
+          activityName: matchActivity.name,
+        });
+        printLazyComponentPromiseError({
+          promiseResult: lazyComponentPromiseResult,
+          activityName: matchActivity.name,
+        });
+      });
+
+      if (shouldWait) {
+        pause();
+        settled.finally(() => {
+          resume();
+        });
+      }
+    }

Nit (optional): filter undefined inputs to Promise.allSettled to avoid creating extraneous fulfilled entries:

const promises = [loaderDataPromise, lazyComponentPromise].filter(
  Boolean,
) as Promise<unknown>[];
const settled = Promise.allSettled(promises)...
extensions/plugin-history-sync/src/historySyncPlugin.tsx (1)

186-196: Remove brittle trailing slash and guard against null pattern match.

Appending a trailing slash to the UrlPattern can cause mismatches for paths without trailing slashes. Also, UrlPattern.match() can return null; spreading null into params will throw. Fix both.

Apply this diff:

-        const pattern = new UrlPattern(
-          `${targetActivityRoute.path}/`,
-          options.urlPatternOptions,
-        );
+        const pattern = new UrlPattern(
+          targetActivityRoute.path,
+          options.urlPatternOptions,
+        );
         const url = pathToUrl(currentPath);
-        const pathParams = pattern.match(url.pathname);
+        const pathParams = pattern.match(url.pathname) ?? {};
         const searchParams = urlSearchParamsToMap(url.searchParams);
         const params = {
           ...searchParams,
           ...pathParams,
         };

Optional: to avoid dual parsing, reuse makeTemplate(targetActivityRoute, ...).parse(currentPath) and merge with search params, as you already do later at Lines 260-266.

🧹 Nitpick comments (6)
integrations/react/src/future/loader/loaderPlugin.tsx (1)

121-124: Avoid any-cast for shouldRenderImmediately; add a minimal typed guard

Direct any-casts reduce type safety. Introduce a tiny local type or a type guard for lazyActivityComponentRenderContext to keep inference intact without widening.

Example:

type LazyActivityComponentRenderContext = {
  shouldRenderImmediately?: boolean;
};

const shouldRenderImmediately =
  (activityContext as { lazyActivityComponentRenderContext?: LazyActivityComponentRenderContext })
    ?.lazyActivityComponentRenderContext?.shouldRenderImmediately;
extensions/plugin-history-sync/src/historySyncPlugin.tsx (5)

98-115: Drain the default-history queue while idle and schedule retries to avoid stalls.

As written, only one task executes per checkpoint and progress relies on onChanged frequency. If onChanged isn’t dispatched for some steps (or transitions stay mostly idle), tasks can stall.

Apply this diff to drain multiple tasks while idle and schedule a retry when still pending:

 function defaultHistorySetupCheckpoint(actions: StackflowActions) {
-  if (!pendingDefaultHistoryEntryInsertionTasks) return;
-
-  const stack = actions.getStack();
-
-  if (stack.globalTransitionState !== "idle") return;
-
-  const nextTask =
-    pendingDefaultHistoryEntryInsertionTasks.shift();
-
-  if (pendingDefaultHistoryEntryInsertionTasks.length === 0) {
-    pendingDefaultHistoryEntryInsertionTasks = null;
-  }
-
-  if (nextTask) {
-    nextTask(actions);
-  }
+  if (!pendingDefaultHistoryEntryInsertionTasks) return;
+  // Drain as many tasks as we can while the stack is idle.
+  while (
+    pendingDefaultHistoryEntryInsertionTasks &&
+    pendingDefaultHistoryEntryInsertionTasks.length > 0 &&
+    actions.getStack().globalTransitionState === "idle"
+  ) {
+    const nextTask =
+      pendingDefaultHistoryEntryInsertionTasks.shift();
+    if (nextTask) nextTask(actions);
+  }
+  if (
+    pendingDefaultHistoryEntryInsertionTasks &&
+    pendingDefaultHistoryEntryInsertionTasks.length === 0
+  ) {
+    pendingDefaultHistoryEntryInsertionTasks = null;
+  } else if (pendingDefaultHistoryEntryInsertionTasks) {
+    // Ensure forward progress even if onChanged is sparse.
+    requestHistoryTick(() => defaultHistorySetupCheckpoint(actions));
+  }
 }

490-496: Abort policy on unexpected pushes looks correct; consider resilience.

Clearing pending tasks when an unrecognized push occurs is a sensible abort. If you encounter environments where onChanged isn’t reliably dispatched after these pushes, couple this with a scheduled checkpoint (see earlier suggestion) to ensure the queue progresses.


521-527: Mirrored abort policy for step pushes is consistent.

Same observation as onPushed; optional scheduled checkpoint can help in sparse onChanged scenarios.


618-635: Nit: simplify the path-presence check for readability.

Current condition relies on operator precedence. Make it explicit for maintainability.

Apply this diff:

-        if (
-          !actionParams.activityContext ||
-          "path" in actionParams.activityContext === false
-        ) {
+        if (
+          !actionParams.activityContext ||
+          !("path" in actionParams.activityContext)
+        ) {

641-658: Nit: same readability improvement here.

Apply this diff:

-        if (
-          !actionParams.activityContext ||
-          "path" in actionParams.activityContext === false
-        ) {
+        if (
+          !actionParams.activityContext ||
+          !("path" in actionParams.activityContext)
+        ) {
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • 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 f85871e and 793f1f9.

📒 Files selected for processing (3)
  • extensions/plugin-history-sync/src/historySyncPlugin.tsx (13 hunks)
  • integrations/react/src/future/ActivityLayoutComponentType.ts (1 hunks)
  • integrations/react/src/future/loader/loaderPlugin.tsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • integrations/react/src/future/ActivityLayoutComponentType.ts
⏰ 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). (1)
  • GitHub Check: Workers Builds: stackflow-docs
🔇 Additional comments (12)
integrations/react/src/future/loader/loaderPlugin.tsx (1)

121-124: Good: preloading the content component (when structured) vs. monolithic

Using contentComponent as the lazy target ensures you prefetch the actual content for structured activities. This aligns with the structured component design and avoids loading non-content wrappers.

extensions/plugin-history-sync/src/historySyncPlugin.tsx (11)

6-11: Imports look good; type import for StackflowActions is appropriate.

Bringing StackflowActions in as a type keeps runtime bundle lean. Adding UrlPattern directly here is fine, but ensure the package declares url-pattern as a dependency for this package.

If not already present, add "url-pattern" to extensions/plugin-history-sync/package.json dependencies.


91-120: Queue + entity set design is solid; consider small ergonomics.

The queue (pendingDefaultHistoryEntryInsertionTasks) and entity set (defaultHistoryEntryEntities) are a good basis for deterministic default-history seeding. clearPendingDefaultHistoryEntryInsertionTasks correctly resets both.


153-166: Initial path resolution is correct and SSR-aware.

Prefers initialContext.req.path in SSR, then hash-based path, then pathname+search. Good coverage.


169-185: Route resolution with fallback is clear and robust.

Using makeTemplate.parse for match and falling back to the configured activity name is straightforward and keeps behavior predictable.


287-306: Great: initial default-history activity id is pre-registered and reused.

This addresses the earlier sequencing/race where onPushed would clear pending tasks because the initial id wasn’t recognized.


309-316: Good null-guard with search param fallback for activity params.

This ensures fallback activities still receive query params when template parsing returns null.


346-356: Replace state tick on init is correct and safely silenced.

requestHistoryTick around replaceState with silentFlag prevents feedback loops. Solid.


487-488: Checkpoint trigger after listener wiring is a good kick-off.

This ensures pending default-history tasks start processing without waiting for the first onChanged.


553-556: Clearing pending tasks on replace/pop is a safe cleanup.

This prevents stale default-history insertions after user-driven navigation changes.

Also applies to: 580-583, 607-616


730-732: onChanged hook is put to good use.

This keeps default-history processing responsive to state changes.


200-245: Verify whether activityContext.path should reflect each default-history entry’s own route.

Right now, all queued pushes during default-history seeding use activityContext.path = currentPath. If other parts of the system rely on activityContext.path for route reconstruction or analytics, you may want to fill it per-entry using that entry’s route template and params.

I can draft a follow-up that, for each entry, resolves its route and fills the path accordingly. Do you want that change?

Also applies to: 260-284

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 (2)
extensions/plugin-history-sync/src/historySyncPlugin.tsx (2)

288-299: Good fix: Seed initialActivityId in default-history entity set.

This resolves the race where the first onPushed could otherwise clear queued tasks. Thanks for addressing the earlier sequencing bug.


186-196: Fix null-spread crash and brittle trailing-slash match; reuse makeTemplate for params.

UrlPattern.match() returns null on mismatch. Spreading ...pathParams when it's null will throw at runtime. The hard-coded trailing slash also makes matching brittle. Build params via makeTemplate.parse and guard null; merge query params explicitly.

Apply this diff:

-        const pattern = new UrlPattern(
-          `${targetActivityRoute.path}/`,
-          options.urlPatternOptions,
-        );
-        const url = pathToUrl(currentPath);
-        const pathParams = pattern.match(url.pathname);
-        const searchParams = urlSearchParamsToMap(url.searchParams);
-        const params = {
-          ...searchParams,
-          ...pathParams,
-        };
+        const url = pathToUrl(currentPath);
+        const paramsTemplate = makeTemplate(
+          targetActivityRoute,
+          options.urlPatternOptions,
+        );
+        const pathParams = paramsTemplate.parse(currentPath) ?? {};
+        const searchParams = urlSearchParamsToMap(url.searchParams);
+        const params = {
+          ...searchParams,
+          ...pathParams,
+        };
🧹 Nitpick comments (2)
extensions/plugin-history-sync/src/historySyncPlugin.tsx (2)

205-213: Remove non-null assertion on additionalSteps; iterate safely.

Even though the enclosing length check makes it practically safe today, the ! is unnecessary and brittle. Use a null-safe iteration, consistent with your later code.

-                    } of initialHistoryEntry.additionalSteps!) {
+                    } of initialHistoryEntry.additionalSteps ?? []) {

98-114: Optionally chain checkpoints to reduce reliance on onChanged cadence.

Today you process one task per checkpoint and rely on onChanged to drive the next. To improve resilience when onChanged is sparse, immediately schedule another checkpoint after running a task.

       if (nextTask) {
-        nextTask(actions);
+        nextTask(actions);
+        // Process the next task if we're still idle.
+        defaultHistorySetupCheckpoint(actions);
       }

If you want to avoid immediate recursion, wrap the follow-up call with requestHistoryTick.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • 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 793f1f9 and b174cd9.

📒 Files selected for processing (1)
  • extensions/plugin-history-sync/src/historySyncPlugin.tsx (13 hunks)
⏰ 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). (6)
  • GitHub Check: Workers Builds: stackflow-docs
  • GitHub Check: Check the status of Changesets
  • GitHub Check: Check the TypeScript typings
  • GitHub Check: Build all packages
  • GitHub Check: Create PR or release packages
  • GitHub Check: Check whether the written test passes normally
🔇 Additional comments (5)
extensions/plugin-history-sync/src/historySyncPlugin.tsx (5)

260-285: Good: Fallback to query params when parse returns null.

This null-safe merge prevents crashes for fallback routes and preserves query params for deep links. Nicely handled.


488-489: LGTM: Checkpoint triggers wired at init and onChanged.

These hooks ensure the default-history queue makes progress as the stack transitions.

Also applies to: 731-733


491-497: LGTM: Defensive clearing of pending default-history tasks.

Clearing on mismatched pushes, steps, replacements, and pops prevents default-history invariants from leaking into user-driven navigation.

Also applies to: 522-528, 554-557, 581-584, 608-617


619-637: LGTM: Fill missing activityContext.path on push/replace.

The conditional override preserves explicit paths and ensures history state stays aligned with route params when omitted.

Also applies to: 642-659


293-299: Guard possible undefined activityParams in initial default-history event.

If a route’s defaultHistory omits activityParams for the initial entry, spreading will throw. Make it null-safe.

             makeEvent("Pushed", {
               activityId: initialActivityId,
               activityName: initialHistoryEntry.activityName,
               activityParams: {
-                ...initialHistoryEntry.activityParams,
+                ...(initialHistoryEntry.activityParams ?? {}),
               },
               eventDate: enoughPastTime,
               activityContext: {

Likely an incorrect or invalid review comment.

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 (2)
extensions/plugin-history-sync/src/historySyncPlugin.tsx (2)

288-295: LGTM: initial activity id is recorded before the initial Pushed event

Registering initialActivityId in defaultHistoryEntryEntities prevents premature clearing of pending default-history tasks. This resolves the sequencing race noted earlier.


246-247: LGTM: safely iterating additionalSteps for subsequent entries

The additionalSteps ?? [] guard avoids runtime errors when steps are absent.

🧹 Nitpick comments (2)
extensions/plugin-history-sync/src/historySyncPlugin.tsx (2)

98-114: Make the checkpoint self-progressing to avoid stalling when onChanged is sparse

Today, one task is executed per checkpoint and we rely on onChanged/external triggers to continue. To make progress more robust (e.g., when intermediate events don’t fire onChanged reliably), re-schedule the next checkpoint after running a task:

     function defaultHistorySetupCheckpoint(actions: StackflowActions) {
       if (!pendingDefaultHistoryEntryInsertionTasks) return;

       const stack = actions.getStack();

       if (stack.globalTransitionState !== "idle") return;

       const nextTask = pendingDefaultHistoryEntryInsertionTasks.shift();

       if (pendingDefaultHistoryEntryInsertionTasks.length === 0) {
         pendingDefaultHistoryEntryInsertionTasks = null;
       }

       if (nextTask) {
         nextTask(actions);
+        // Attempt another checkpoint on the next history tick.
+        // If the stack isn't idle yet, the guard above will prevent progress until it is.
+        requestHistoryTick(() => defaultHistorySetupCheckpoint(actions));
       }
     }

This keeps default-history insertion moving without waiting on incidental lifecycle calls.


94-94: Widen entity-id type for safety

defaultHistoryEntryEntities stores ids from id(), activity.id, and step.id. If these are not strictly string (some stacks use numeric ids), the Set<string> type can become misleading. Prefer typing by generator return type:

-    const defaultHistoryEntryEntities: Set<string> = new Set();
+    const defaultHistoryEntryEntities: Set<ReturnType<typeof id>> = new Set();

This keeps the set compatible even if the id type changes across versions.

If you prefer explicit types, verify the actual id type in your setup by checking typeof id() at runtime or referencing the exported type in @stackflow/core.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • 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 b174cd9 and 658ccd5.

📒 Files selected for processing (1)
  • extensions/plugin-history-sync/src/historySyncPlugin.tsx (13 hunks)
⏰ 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). (5)
  • GitHub Check: Build all packages
  • GitHub Check: Check whether the written test passes normally
  • GitHub Check: Check the TypeScript typings
  • GitHub Check: Create PR or release packages
  • GitHub Check: Workers Builds: stackflow-docs
🔇 Additional comments (4)
extensions/plugin-history-sync/src/historySyncPlugin.tsx (4)

260-285: LGTM: fallback query-params are preserved for the synthetic push

Good change to guard template.parse(currentPath) and fall back to urlSearchParams so deep-linked query params are not dropped on the final synthetic push.


618-637: Path autofill is correct; preserves explicit paths

Conditionally filling activityContext.path only when not provided maintains caller intent and keeps history URLs consistent with params.


296-298: Guard against undefined/null activityParams in initial default-history push

If a route author omits activityParams on the first defaultHistory entry, spreading it will throw. Add a nullish fallback:

-              activityParams: {
-                ...initialHistoryEntry.activityParams,
-              },
+              activityParams: {
+                ...(initialHistoryEntry.activityParams ?? {}),
+              },

Likely an incorrect or invalid review comment.


186-196: The initial assumption that spreading null crashes in JavaScript is incorrect: object spread of null or undefined simply results in no properties and does not throw (ES2018 spec) — so a runtime guard for pathParams isn’t required to prevent an exception. However, reusing an existing makeTemplate–based parser to avoid duplicated URL logic remains valid if makeTemplate is available and intended for this route’s parsing.

Next steps:

• Verify that makeTemplate is imported and returns a parser compatible with template.parse(path).
• If makeTemplate already handles both matching and parameter extraction, refactor to use it exclusively.
• Otherwise, drop the incorrect null-guard suggestion and simply null-coalesce the result of pattern.match() to avoid undefined merging.

Rewrite summary:

Use makeTemplate parser if available; remove incorrect null-spread guard

  • Confirm and import makeTemplate from the module that provides it.
  • Replace the dual new UrlPattern + pattern.match() calls with a single template.parse(currentPath) if supported.
  • Remove the “remove UrlPattern import” only if UrlPattern is no longer used anywhere else in this file.
  • Otherwise, keep the existing code and simply default pathParams to {}:
const pattern = new UrlPattern(
  `${targetActivityRoute.path}(/)`,
  options.urlPatternOptions,
);
const url = pathToUrl(currentPath);
const pathParams = pattern.match(url.pathname) ?? {};
const searchParams = urlSearchParamsToMap(url.searchParams);
const params = {
  ...searchParams,
  ...pathParams,
};

This approach corrects the null guard without introducing incorrect assumptions about crashes.

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: 1

♻️ Duplicate comments (3)
extensions/plugin-history-sync/src/historySyncPlugin.tsx (1)

186-196: Fix null spread crash when building params for defaultHistory.

pattern.match(url.pathname) can return null for fallback routes; spreading null into an object throws at runtime. Reuse makeTemplate and guard with a search-params fallback.

-        const pattern = new UrlPattern(
-          `${targetActivityRoute.path}(/)`,
-          options.urlPatternOptions,
-        );
-        const url = pathToUrl(currentPath);
-        const pathParams = pattern.match(url.pathname);
-        const searchParams = urlSearchParamsToMap(url.searchParams);
-        const params = {
-          ...searchParams,
-          ...pathParams,
-        };
+        const templateForParams = makeTemplate(
+          targetActivityRoute,
+          options.urlPatternOptions,
+        );
+        const params =
+          templateForParams.parse(currentPath) ??
+          urlSearchParamsToMap(pathToUrl(currentPath).searchParams);
integrations/react/src/future/loader/loaderPlugin.tsx (1)

124-148: Always attach handlers to loader/lazy promises; pause/resume only when waiting

Unhandled rejections can occur when rendering immediately with a component-level loading UI (no handlers are attached). Attach allSettled unconditionally and gate pause/resume by shouldWait. This also preserves your corrected "loading" in matchActivityComponent check.

Apply:

-    const shouldRenderImmediately = (activityContext as any)
-      ?.lazyActivityComponentRenderContext?.shouldRenderImmediately;
-
-    if (
-      (loaderDataPromise || lazyComponentPromise) &&
-      (shouldRenderImmediately !== true ||
-        "loading" in matchActivityComponent === false)
-    ) {
-      pause();
-
-      Promise.allSettled([loaderDataPromise, lazyComponentPromise])
-        .then(([loaderDataPromiseResult, lazyComponentPromiseResult]) => {
-          printLoaderDataPromiseError({
-            promiseResult: loaderDataPromiseResult,
-            activityName: matchActivity.name,
-          });
-          printLazyComponentPromiseError({
-            promiseResult: lazyComponentPromiseResult,
-            activityName: matchActivity.name,
-          });
-        })
-        .finally(() => {
-          resume();
-        });
-    }
+    const shouldRenderImmediately = (activityContext as any)
+      ?.lazyActivityComponentRenderContext?.shouldRenderImmediately;
+
+    const shouldWait =
+      (loaderDataPromise || lazyComponentPromise) &&
+      (shouldRenderImmediately !== true ||
+        "loading" in matchActivityComponent === false);
+
+    const settled = Promise.allSettled([
+      loaderDataPromise,
+      lazyComponentPromise,
+    ]).then(([loaderDataPromiseResult, lazyComponentPromiseResult]) => {
+      printLoaderDataPromiseError({
+        promiseResult: loaderDataPromiseResult,
+        activityName: matchActivity.name,
+      });
+      printLazyComponentPromiseError({
+        promiseResult: lazyComponentPromiseResult,
+        activityName: matchActivity.name,
+      });
+    });
+
+    if (shouldWait) {
+      pause();
+      settled.finally(() => {
+        resume();
+      });
+    }
integrations/react/src/__internal__/PluginRenderer.tsx (1)

102-110: Remount ErrorBoundary on param changes to clear stale error state

Without a key, an error from prior params can persist after navigation. Add a stable key derived from params.

-      errorHandler?.component ? (
-        <StructuredActivityComponentErrorBoundary
+      errorHandler?.component ? (
+        <StructuredActivityComponentErrorBoundary
+          key={JSON.stringify(params)}
           renderFallback={(err, reset) => (
             <errorHandler.component params={params} error={err} reset={reset} />
           )}
         >
           {node}
         </StructuredActivityComponentErrorBoundary>
       ) : (
         node
       ),
🧹 Nitpick comments (10)
demo/src/activities/Article/Article.layout.tsx (1)

5-5: Guard against undefined title to avoid an empty app bar label.

If title is optional (as per the Register augmentation), pass a fallback to prevent rendering “undefined”.

Apply this diff:

-  return <Layout appBar={{ title }}>{children}</Layout>;
+  return <Layout appBar={{ title: title ?? "Article" }}>{children}</Layout>;

If Layout already accepts title?: string, feel free to skip.

demo/src/activities/Article/Article.tsx (1)

14-18: Add an error handler to surface content-load failures gracefully.

Optional but improves UX and observability when Article.content fails or throws.

Apply this diff:

 export const Article = structuredActivityComponent<"Article">({
   content: () => import("./Article.content"),
   layout: ArticleLayout,
   loading: ArticleLoading,
+  errorHandler: ({ error }) => (
+    <div role="alert" style={{ padding: 16 }}>
+      Failed to load article.
+    </div>
+  ),
 });
extensions/plugin-history-sync/src/historySyncPlugin.tsx (3)

11-11: Remove UrlPattern import (no longer needed after the fix).

Keeps bundle lean and avoids unused import warnings.

-import UrlPattern from "url-pattern";

205-213: Avoid non-null assertion on additionalSteps.

Guard directly in the loop to drop the ! and simplify.

-                    for (const {
-                      stepParams,
-                      hasZIndex,
-                    } of initialHistoryEntry.additionalSteps!) {
+                    for (const { stepParams, hasZIndex } of (initialHistoryEntry.additionalSteps ?? [])) {

678-694: Double back calls on replace: confirm intent.

Each iteration issues two history.back() calls (one conditional, one unconditional). If intentional to collapse both activity and step states, add a short comment; otherwise this may overshoot.

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

60-82: Align lazy error semantics with existing lazy wrapper (optional).

React.lazy caches rejections, making retries harder. Consider reusing your project’s lazy wrapper (future/lazy.tsx) semantics for reset-on-error consistency, or memoize the loader similarly.


35-43: Tighten brand check.

Also verify the branded flag is true, not just present.

-  return (
-    value !== null &&
-    typeof value === "object" &&
-    STRUCTURED_ACTIVITY_COMPONENT_TYPE in value
-  );
+  return (
+    value !== null &&
+    typeof value === "object" &&
+    (value as any)[STRUCTURED_ACTIVITY_COMPONENT_TYPE] === true
+  );
integrations/react/src/future/loader/loaderPlugin.tsx (1)

111-116: Guard synchronous loader throws

loadData may throw synchronously, which would bypass your promise error logging and break routing. Wrap with try/catch and log consistently.

-    const loaderData =
-      matchActivity.loader && loadData(activityName, activityParams);
+    let loaderData: unknown;
+    try {
+      loaderData =
+        matchActivity.loader && loadData(activityName, activityParams);
+    } catch (err) {
+      console.error(err);
+      console.error(
+        `The above error occurred in the "${matchActivity.name}" activity loader (sync throw).`,
+      );
+      loaderData = undefined;
+    }
integrations/react/src/__internal__/PluginRenderer.tsx (2)

127-158: Optional: log caught errors for visibility

Consider adding componentDidCatch to surface errors that reach the boundary (helps debugging beyond the loader logs).

 class StructuredActivityComponentErrorBoundary extends Component<
@@
   static getDerivedStateFromError(error: unknown): {
     hasError: true;
     error: unknown;
   } {
     return { hasError: true, error };
   }
+
+  componentDidCatch(error: unknown) {
+    // Keep lightweight; defer advanced reporting to host app
+    console.error(error);
+  }
 }

85-91: Tighten typings for params

params: {} erases safety. If generic threading is difficult here, prefer Record<string, unknown> to reflect runtime shape.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • 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 658ccd5 and 9d4b11a.

📒 Files selected for processing (10)
  • demo/src/activities/Article/Article.content.tsx (1 hunks)
  • demo/src/activities/Article/Article.layout.tsx (1 hunks)
  • demo/src/activities/Article/Article.loading.tsx (1 hunks)
  • demo/src/activities/Article/Article.tsx (1 hunks)
  • extensions/plugin-history-sync/src/historySyncPlugin.tsx (13 hunks)
  • integrations/react/src/__internal__/PluginRenderer.tsx (3 hunks)
  • integrations/react/src/__internal__/StructuredActivityComponentType.tsx (1 hunks)
  • integrations/react/src/future/index.ts (1 hunks)
  • integrations/react/src/future/loader/loaderPlugin.tsx (2 hunks)
  • integrations/react/src/future/usePrepare.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • demo/src/activities/Article/Article.loading.tsx
  • demo/src/activities/Article/Article.content.tsx
  • integrations/react/src/future/usePrepare.ts
🧰 Additional context used
🧬 Code graph analysis (6)
demo/src/activities/Article/Article.tsx (2)
config/src/Register.ts (1)
  • Register (2-2)
integrations/react/src/__internal__/StructuredActivityComponentType.tsx (1)
  • structuredActivityComponent (19-33)
demo/src/activities/Article/Article.layout.tsx (1)
integrations/react/src/__internal__/StructuredActivityComponentType.tsx (2)
  • layout (88-95)
  • Layout (84-86)
integrations/react/src/__internal__/StructuredActivityComponentType.tsx (3)
config/src/RegisteredActivityName.ts (1)
  • RegisteredActivityName (3-6)
config/src/InferActivityParams.ts (1)
  • InferActivityParams (5-8)
integrations/react/src/future/lazy.tsx (1)
  • lazy (5-28)
extensions/plugin-history-sync/src/historySyncPlugin.tsx (4)
core/src/interfaces/StackflowActions.ts (1)
  • StackflowActions (15-65)
extensions/plugin-history-sync/src/makeHistoryTaskQueue.ts (1)
  • makeHistoryTaskQueue (7-30)
extensions/plugin-history-sync/src/makeTemplate.ts (3)
  • makeTemplate (48-110)
  • pathToUrl (5-7)
  • urlSearchParamsToMap (9-17)
core/src/event-utils/makeEvent.ts (1)
  • makeEvent (5-19)
integrations/react/src/future/loader/loaderPlugin.tsx (2)
integrations/react/src/__internal__/StructuredActivityComponentType.tsx (1)
  • isStructuredActivityComponent (35-43)
core/src/utils/makeActions.ts (2)
  • pause (100-113)
  • resume (114-127)
integrations/react/src/__internal__/PluginRenderer.tsx (2)
integrations/react/src/__internal__/StructuredActivityComponentType.tsx (7)
  • isStructuredActivityComponent (35-43)
  • structuredActivityComponent (19-33)
  • StructuredActivityComponentType (11-17)
  • getContentComponent (60-82)
  • loading (101-105)
  • errorHandler (111-119)
  • layout (88-95)
core/src/Stack.ts (1)
  • Activity (28-43)
⏰ 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). (4)
  • GitHub Check: Build all packages
  • GitHub Check: Check whether the written test passes normally
  • GitHub Check: Create PR or release packages
  • GitHub Check: Workers Builds: stackflow-docs
🔇 Additional comments (8)
demo/src/activities/Article/Article.layout.tsx (1)

4-6: Clean use of layout HOC; props destructuring and children passthrough look solid.

demo/src/activities/Article/Article.tsx (1)

5-12: Module augmentation for route params is correct and scoped; nice.

extensions/plugin-history-sync/src/historySyncPlugin.tsx (1)

488-495: Nice: checkpoint gating is sensible.

Only progressing the default-history queue when the stack consists solely of default entries is a good guard; else clearing tasks prevents unintended insertions after user actions.

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

19-33: LGTM: structured component factory is clean and type-safe.

Branding via a unique symbol and preserving option types looks good.

integrations/react/src/future/index.ts (1)

3-3: LGTM: re-export surfaces the new structured API.

Makes the structured helpers available to consumers from a stable entry.

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

7-7: Import looks correct

Structured-activity detection is correctly wired for lazy prefetch decisions.

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

92-119: Correct wrapper order (Suspense inside ErrorBoundary inside Layout)

Good fix: renders as Layout(ErrorBoundary(Suspense(Content))). This ensures lazy-load errors are caught and layout wraps final UI.


42-47: Structured vs. function component dispatch looks good

The branch correctly selects structured rendering when applicable.

anakin_karrot added 3 commits September 5, 2025 16:11
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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
integrations/react/src/stable/stackflow.tsx (1)

310-323: Bug: replace() returns a different activityId than the one dispatched

You compute activityId but dispatch with a new one. The returned ID won’t match the stack entry.

Fix:

-      replace(activityName, activityParams, options) {
-        const activityId = options?.activityId ?? makeActivityId();
+      replace(activityName, activityParams, options) {
+        const activityId = options?.activityId ?? makeActivityId();
         getCoreStore()?.actions.replace({
-          activityId: options?.activityId ?? makeActivityId(),
+          activityId,
           activityName,
           activityParams,
           skipEnterActiveState: parseActionOptions(options).skipActiveState,
         });
         return {
           activityId,
         };
       },
integrations/react/src/stable/useActions.ts (1)

94-106: Bug: replace() returns an activityId different from the one dispatched

Same mismatch as in stackflow.tsx. Return value won’t correspond to the replaced entry.

Apply:

-      replace(activityName, activityParams, options) {
-        const activityId = makeActivityId();
+      replace(activityName, activityParams, options) {
+        const activityId = options?.activityId ?? makeActivityId();
         coreActions?.replace({
-          activityId: options?.activityId ?? makeActivityId(),
+          activityId,
           activityName,
           activityParams,
           skipEnterActiveState: parseActionOptions(options).skipActiveState,
         });
         return {
           activityId,
         };
       },
🧹 Nitpick comments (2)
integrations/react/src/stable/stackflow.tsx (1)

136-144: Build the map without repeated object spreads

Minor perf/readability: avoid O(n²) spreads in reduce.

Apply:

-  const activityComponentMap = Object.entries(options.activities).reduce(
-    (acc, [key, Activity]) => ({
-      ...acc,
-      [key]: "component" in Activity ? Activity.component : Activity,
-    }),
-    {} as {
-      [key: string]: MonolithicActivityComponentType;
-    },
-  );
+  const activityComponentMap: { [key: string]: MonolithicActivityComponentType } = {};
+  for (const [key, Activity] of Object.entries(options.activities)) {
+    activityComponentMap[key] =
+      "component" in Activity ? Activity.component : (Activity as MonolithicActivityComponentType);
+  }
integrations/react/src/stable/index.ts (1)

3-3: Use type-only re-export to avoid accidental value export

Purely a type alias; prefer export type to keep emit clean.

-export { MonolithicActivityComponentType as ActivityComponentType } from "../__internal__/MonolithicActivityComponentType";
+export type { MonolithicActivityComponentType as ActivityComponentType } from "../__internal__/MonolithicActivityComponentType";
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • 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 9d4b11a and d65e0c6.

📒 Files selected for processing (4)
  • integrations/react/src/stable/BaseActivities.ts (1 hunks)
  • integrations/react/src/stable/index.ts (1 hunks)
  • integrations/react/src/stable/stackflow.tsx (5 hunks)
  • integrations/react/src/stable/useActions.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
integrations/react/src/stable/useActions.ts (1)
integrations/react/src/stable/index.ts (1)
  • MonolithicActivityComponentType (3-3)
integrations/react/src/stable/BaseActivities.ts (1)
integrations/react/src/stable/index.ts (1)
  • MonolithicActivityComponentType (3-3)
integrations/react/src/stable/stackflow.tsx (2)
integrations/react/src/stable/index.ts (1)
  • MonolithicActivityComponentType (3-3)
integrations/react/src/__internal__/MonolithicActivityComponentType.ts (1)
  • MonolithicActivityComponentType (4-6)
⏰ 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). (1)
  • GitHub Check: Workers Builds: stackflow-docs
🔇 Additional comments (8)
integrations/react/src/stable/stackflow.tsx (3)

94-102: Type inference switchover to MonolithicActivityComponentType looks good

The conditional inference for useStepFlow parameters matches the new unified type.


109-111: Public addActivity typing aligned with MonolithicActivityComponentType

Consistent with the new public surface. Good.


261-262: Assigning the raw component is correct

This matches the map’s value type and earlier construction.

integrations/react/src/stable/BaseActivities.ts (2)

2-2: Import update to MonolithicActivityComponentType is consistent

Matches the new unified component type.


6-12: BaseActivities union updated correctly

Both “monolithic” and { component, paramsSchema } forms are typed consistently.

integrations/react/src/stable/useActions.ts (3)

4-4: Import update aligns with new unified type

Looks good.


31-38: Param inference switch to MonolithicActivityComponentType (push) is correct

Type inference remains intact.


48-55: Param inference switch to MonolithicActivityComponentType (replace) is correct

Consistent with the new public surface.

@ENvironmentSet ENvironmentSet merged commit f298988 into main Sep 5, 2025
7 of 8 checks passed
@ENvironmentSet ENvironmentSet deleted the deep-link-default-nav-context branch September 5, 2025 07:19
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.

2 participants