fix(plugin-history-sync): only call fallbackActivity when no route matches#707
fix(plugin-history-sync): only call fallbackActivity when no route matches#707
Conversation
…tches Move the fallbackActivity callback invocation back inside the no-match branch so it is no longer called on every initialization. This restores the pre-1.8.0 contract where the callback is only invoked when currentPath does not match any registered route. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: eb2ef69 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
stackflow-docs | eb2ef69 | Commit Preview URL | Apr 30 2026, 09:39 AM |
|
Caution Review failedPull request was closed or merged during review No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe changes ensure Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@extensions/plugin-history-sync/src/historySyncPlugin.spec.ts`:
- Around line 173-219: The tests use a fallbackActivity mock that must match the
typed signature (args: { initialContext: any }) => K; update both jest.fn mocks
in the failing tests to accept the callback arg (e.g. jest.fn((_args) => "Home")
or jest.fn(({ initialContext }) => "Home")) so the mock is assignable to the
fallbackActivity parameter passed into historySyncPlugin when calling stackflow;
ensure both occurrences in the tests that define fallbackActivity are changed
accordingly.
In `@extensions/plugin-history-sync/src/historySyncPlugin.tsx`:
- Around line 240-246: The code calls options.fallbackActivity({ initialContext
}) inside the activityRoutes.find predicate causing repeated evaluation; compute
the fallback once into a const (e.g., const fallbackName =
options.fallbackActivity({ initialContext })) before computing
targetActivityRoute and then use fallbackName in the find call instead of
invoking options.fallbackActivity again; update the targetActivityRoute
expression that references matchedActivityRoute, activityRoutes.find(...), and
options.fallbackActivity to use the cached fallbackName.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 06ecf61d-1de3-42ff-b276-b37c308c79f9
📒 Files selected for processing (3)
.changeset/fix-fallback-activity-called-on-match.mdextensions/plugin-history-sync/src/historySyncPlugin.spec.tsextensions/plugin-history-sync/src/historySyncPlugin.tsx
| test("historySyncPlugin - 초기에 매칭하는 라우트가 있으면 fallbackActivity 콜백을 호출하지 않습니다", async () => { | ||
| history = createMemoryHistory({ | ||
| initialEntries: ["/articles/123"], | ||
| }); | ||
|
|
||
| const fallbackActivity = jest.fn(() => "Home"); | ||
|
|
||
| stackflow({ | ||
| activityNames: ["Home", "Article"], | ||
| plugins: [ | ||
| historySyncPlugin({ | ||
| history, | ||
| routes: { | ||
| Home: "/home", | ||
| Article: "/articles/:articleId", | ||
| }, | ||
| fallbackActivity, | ||
| }), | ||
| ], | ||
| }); | ||
|
|
||
| expect(fallbackActivity).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| test("historySyncPlugin - 초기에 매칭하는 라우트가 없으면 fallbackActivity 콜백을 호출합니다", async () => { | ||
| history = createMemoryHistory({ | ||
| initialEntries: ["/non-existent-path"], | ||
| }); | ||
|
|
||
| const fallbackActivity = jest.fn(() => "Home"); | ||
|
|
||
| stackflow({ | ||
| activityNames: ["Home", "Article"], | ||
| plugins: [ | ||
| historySyncPlugin({ | ||
| history, | ||
| routes: { | ||
| Home: "/home", | ||
| Article: "/articles/:articleId", | ||
| }, | ||
| fallbackActivity, | ||
| }), | ||
| ], | ||
| }); | ||
|
|
||
| expect(fallbackActivity).toHaveBeenCalled(); | ||
| }); |
There was a problem hiding this comment.
Make the fallback mocks accept the callback argument.
fallbackActivity is typed as (args: { initialContext: any }) => K, but both new tests use jest.fn(() => "Home"). That no-arg mock is not assignable here, which matches the TS2322 failure in CI. Please change both mocks to accept the parameter, e.g. jest.fn((_args) => "Home") or jest.fn(({ initialContext }) => "Home").
🛠️ Suggested fix
- const fallbackActivity = jest.fn(() => "Home");
+ const fallbackActivity = jest.fn((_args) => "Home");
@@
- const fallbackActivity = jest.fn(() => "Home");
+ const fallbackActivity = jest.fn((_args) => "Home");📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| test("historySyncPlugin - 초기에 매칭하는 라우트가 있으면 fallbackActivity 콜백을 호출하지 않습니다", async () => { | |
| history = createMemoryHistory({ | |
| initialEntries: ["/articles/123"], | |
| }); | |
| const fallbackActivity = jest.fn(() => "Home"); | |
| stackflow({ | |
| activityNames: ["Home", "Article"], | |
| plugins: [ | |
| historySyncPlugin({ | |
| history, | |
| routes: { | |
| Home: "/home", | |
| Article: "/articles/:articleId", | |
| }, | |
| fallbackActivity, | |
| }), | |
| ], | |
| }); | |
| expect(fallbackActivity).not.toHaveBeenCalled(); | |
| }); | |
| test("historySyncPlugin - 초기에 매칭하는 라우트가 없으면 fallbackActivity 콜백을 호출합니다", async () => { | |
| history = createMemoryHistory({ | |
| initialEntries: ["/non-existent-path"], | |
| }); | |
| const fallbackActivity = jest.fn(() => "Home"); | |
| stackflow({ | |
| activityNames: ["Home", "Article"], | |
| plugins: [ | |
| historySyncPlugin({ | |
| history, | |
| routes: { | |
| Home: "/home", | |
| Article: "/articles/:articleId", | |
| }, | |
| fallbackActivity, | |
| }), | |
| ], | |
| }); | |
| expect(fallbackActivity).toHaveBeenCalled(); | |
| }); | |
| test("historySyncPlugin - 초기에 매칭하는 라우트가 있으면 fallbackActivity 콜백을 호출하지 않습니다", async () => { | |
| history = createMemoryHistory({ | |
| initialEntries: ["/articles/123"], | |
| }); | |
| const fallbackActivity = jest.fn((_args) => "Home"); | |
| stackflow({ | |
| activityNames: ["Home", "Article"], | |
| plugins: [ | |
| historySyncPlugin({ | |
| history, | |
| routes: { | |
| Home: "/home", | |
| Article: "/articles/:articleId", | |
| }, | |
| fallbackActivity, | |
| }), | |
| ], | |
| }); | |
| expect(fallbackActivity).not.toHaveBeenCalled(); | |
| }); | |
| test("historySyncPlugin - 초기에 매칭하는 라우트가 없으면 fallbackActivity 콜백을 호출합니다", async () => { | |
| history = createMemoryHistory({ | |
| initialEntries: ["/non-existent-path"], | |
| }); | |
| const fallbackActivity = jest.fn((_args) => "Home"); | |
| stackflow({ | |
| activityNames: ["Home", "Article"], | |
| plugins: [ | |
| historySyncPlugin({ | |
| history, | |
| routes: { | |
| Home: "/home", | |
| Article: "/articles/:articleId", | |
| }, | |
| fallbackActivity, | |
| }), | |
| ], | |
| }); | |
| expect(fallbackActivity).toHaveBeenCalled(); | |
| }); |
🧰 Tools
🪛 GitHub Actions: Build
[error] 189-189: TypeScript declaration build failed (tsc). TS2322: Type 'Mock<string, [], any>' is not assignable to type '(args: { initialContext: any; }) => "Home" | "Article"'.
🪛 GitHub Actions: Integration
[error] 189-189: tsc (build:dts) failed with TypeScript error TS2322: Type 'Mock<string, [], any>' is not assignable to type '(args: { initialContext: any; }) => "Home" | "Article"'.
🪛 GitHub Actions: Release
[error] 189-189: TS2322: Type 'Mock<string, [], any>' is not assignable to type '(args: { initialContext: any; }) => "Home" | "Article"'. (tsc --emitDeclarationOnly, step @stackflow/plugin-history-sync::tsc)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@extensions/plugin-history-sync/src/historySyncPlugin.spec.ts` around lines
173 - 219, The tests use a fallbackActivity mock that must match the typed
signature (args: { initialContext: any }) => K; update both jest.fn mocks in the
failing tests to accept the callback arg (e.g. jest.fn((_args) => "Home") or
jest.fn(({ initialContext }) => "Home")) so the mock is assignable to the
fallbackActivity parameter passed into historySyncPlugin when calling stackflow;
ensure both occurrences in the tests that define fallbackActivity are changed
accordingly.
…ivity The K type parameter on historySyncPlugin is constrained to a string literal union of activity names. Using jest.fn(() => "Home") infers Mock<string, ...>, widening the return to string and failing the assignability check. Annotate the return type so the literal narrows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
commit: |
…predicate Calling options.fallbackActivity inside the activityRoutes.find predicate caused it to fire once per route iteration. Hoist the call into an IIFE so it runs at most once per overrideInitialEvents invocation when no route matches. Tightened the regression test to assert exactly one invocation per plugin instance instead of the looser "any call" assertion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
fallbackActivitycallback was being invoked on every initialization regardless of whethercurrentPathmatched a registered route, due to a refactor in feat(react, plugin-history-sync): Default history setup option for rich deep link experiences #610 that hoisted the call out of the no-match branch in order to unify the matched/fallbacktargetActivityRoutefordefaultHistory.??, restoring the original behavior while keepingdefaultHistorysupport intact.Test plan
yarn test historySyncPlugin.spec— 29/29 passing