diff --git a/.changeset/fix-fallback-activity-called-on-match.md b/.changeset/fix-fallback-activity-called-on-match.md new file mode 100644 index 000000000..bd2c4e06b --- /dev/null +++ b/.changeset/fix-fallback-activity-called-on-match.md @@ -0,0 +1,5 @@ +--- +"@stackflow/plugin-history-sync": patch +--- + +Fix `fallbackActivity` callback being invoked on every initialization regardless of route matching outcome. Restored the pre-1.8.0 contract: the callback is now called only when no route matches `currentPath`. Apps that perform side effects in this callback (e.g. Sentry logging for unknown deep links) no longer fire on successful matches. diff --git a/extensions/plugin-history-sync/src/historySyncPlugin.spec.ts b/extensions/plugin-history-sync/src/historySyncPlugin.spec.ts index 5ba28d372..66a1f6b03 100644 --- a/extensions/plugin-history-sync/src/historySyncPlugin.spec.ts +++ b/extensions/plugin-history-sync/src/historySyncPlugin.spec.ts @@ -170,6 +170,55 @@ describe("historySyncPlugin", () => { expect(activeActivity(actions.getStack())?.params.title).toEqual("hello"); }); + test("historySyncPlugin - 초기에 매칭하는 라우트가 있으면 fallbackActivity 콜백을 호출하지 않습니다", async () => { + history = createMemoryHistory({ + initialEntries: ["/articles/123"], + }); + + const fallbackActivity = jest.fn((): "Home" => "Home"); + + stackflow({ + activityNames: ["Home", "Article"], + plugins: [ + historySyncPlugin({ + history, + routes: { + Home: "/home", + Article: "/articles/:articleId", + }, + fallbackActivity, + }), + ], + }); + + expect(fallbackActivity).not.toHaveBeenCalled(); + }); + + test("historySyncPlugin - 초기에 매칭하는 라우트가 없으면 fallbackActivity 콜백을 plugin instance당 한 번만 호출합니다", async () => { + history = createMemoryHistory({ + initialEntries: ["/non-existent-path"], + }); + + const fallbackActivity = jest.fn((): "Home" => "Home"); + + const plugin = historySyncPlugin({ + history, + routes: { + Home: "/home", + Article: "/articles/:articleId", + }, + fallbackActivity, + }); + + const pluginInstance = plugin(); + pluginInstance.overrideInitialEvents?.({ + initialEvents: [], + initialContext: {}, + }); + + expect(fallbackActivity).toHaveBeenCalledTimes(1); + }); + test("historySyncPlugin - actions.push() 후에, URL 상태가 알맞게 바뀝니다", async () => { await actions.push({ activityId: "a1", diff --git a/extensions/plugin-history-sync/src/historySyncPlugin.tsx b/extensions/plugin-history-sync/src/historySyncPlugin.tsx index 2428d0407..02164e725 100644 --- a/extensions/plugin-history-sync/src/historySyncPlugin.tsx +++ b/extensions/plugin-history-sync/src/historySyncPlugin.tsx @@ -228,23 +228,27 @@ export function historySyncPlugin< } const currentPath = resolveCurrentPath(); - const fallbackActivityName = options.fallbackActivity({ - initialContext, + const matchedActivityRoute = activityRoutes.find((activityRoute) => { + const template = makeTemplate( + activityRoute, + options.urlPatternOptions, + ); + const activityParams = template.parse(currentPath); + + return activityParams !== null; }); - const targetActivityRoute = - activityRoutes.find((activityRoute) => { - const template = makeTemplate( - activityRoute, - options.urlPatternOptions, - ); - const activityParams = template.parse(currentPath); - - return activityParams !== null; - }) ?? - activityRoutes.find( + const targetActivityRoute = (() => { + if (matchedActivityRoute) { + return matchedActivityRoute; + } + const fallbackActivityName = options.fallbackActivity({ + initialContext, + }); + return activityRoutes.find( (activityRoute) => activityRoute.activityName === fallbackActivityName, )!; + })(); const pattern = new UrlPattern( `${targetActivityRoute.path}(/)`, options.urlPatternOptions,