From 1330aae70aff2bbbb5c97cd07c18d012bc25cded Mon Sep 17 00:00:00 2001 From: ENvironmentSet Date: Tue, 4 Aug 2026 16:41:03 +0900 Subject: [PATCH 1/2] docs(plugin-history-sync): add release and migration guide (FEP-2669) --- extensions/plugin-history-sync/README.md | 313 +++++++++++++++++++++-- 1 file changed, 288 insertions(+), 25 deletions(-) diff --git a/extensions/plugin-history-sync/README.md b/extensions/plugin-history-sync/README.md index af4c3f44a..c78c0e124 100644 --- a/extensions/plugin-history-sync/README.md +++ b/extensions/plugin-history-sync/README.md @@ -1,61 +1,324 @@ # @stackflow/plugin-history-sync -Synchronizes the stack state with the current browser's history +Stackflow navigation is independent of the browser History API by default. +`@stackflow/plugin-history-sync` connects the Stackflow stack to the browser URL +and history, so browser back and forward navigation follow the same screens as +programmatic Stackflow navigation. -- [Documentation](https://stackflow.so) +## Installation -## Usage +```bash +yarn add @stackflow/plugin-history-sync +``` + +`@stackflow/plugin-history-sync@2` requires `@stackflow/core@3`. + +## Setup + +Define a route for each Activity and add `historySyncPlugin()` to the Stackflow +configuration. ```typescript +// stackflow.config.ts import { defineConfig } from "@stackflow/config"; export const config = defineConfig({ activities: [ { - name: "MyHome", + name: "Home", route: "/", }, { - name: "MyArticle", + name: "Article", route: "/articles/:articleId", }, { - name: "NotFoundPage", + name: "NotFound", route: "/404", }, ], - transitionDuration: 350, }); ``` -```typescript -import { stackflow } from "@stackflow/react"; +```tsx +// stackflow.tsx import { historySyncPlugin } from "@stackflow/plugin-history-sync"; +import { stackflow } from "@stackflow/react"; +import { Article } from "./Article"; +import { Home } from "./Home"; +import { NotFound } from "./NotFound"; import { config } from "./stackflow.config"; -import { MyHome } from "./MyHome"; -import { MyArticle } from "./MyArticle"; -import { NotFoundPage } from "./NotFoundPage"; -const { Stack } = stackflow({ +export const { Stack } = stackflow({ config, components: { - MyHome, - MyArticle, - NotFoundPage, + Home, + Article, + NotFound, }, plugins: [ - // ... historySyncPlugin({ config, - /** - * If a URL that does not correspond to the URL template is given, it moves to the `fallbackActivity`. - */ - fallbackActivity: ({ initialContext }) => "NotFoundPage", - /** - * Uses the hash portion of the URL (i.e. window.location.hash) - */ - useHash: false, + fallbackActivity: () => "NotFound", }), ], }); ``` + +When a fresh stack is created, the plugin selects the Activity whose route +matches the current URL. If no route matches, it uses `fallbackActivity`. + +## Routes + +### Path and query parameters + +Named path segments become Activity parameters. Values not consumed by the +path are written as query parameters. + +```typescript +{ + name: "Article", + route: "/articles/:articleId", +} +``` + +Pushing `Article` with `{ articleId: "42", referrer: "home" }` produces +`/articles/42/?referrer=home`. + +### Multiple paths + +Use an array when more than one path should open the same Activity. More +specific routes take priority over less specific routes when the plugin matches +or creates a URL. + +```typescript +{ + name: "Article", + route: ["/articles/:articleId", "/posts/:articleId"], +} +``` + +### Encoding and decoding parameters + +Use an object route to translate between Activity parameter values and URL +strings. + +```typescript +{ + name: "Article", + route: { + path: "/articles/:articleId", + encode: ({ articleId, preview }) => ({ + articleId: String(articleId), + preview: preview ? "true" : undefined, + }), + decode: ({ articleId, preview }) => ({ + articleId: Number(articleId), + preview: preview === "true", + }), + }, +} +``` + +`encode` runs when Stackflow navigation produces a URL. `decode` runs when a +URL produces Activity parameters. Without these functions, URL parameters are +strings. + +### Default history + +`defaultHistory` inserts Activities below a directly opened Activity. This can +give deep links a useful browser-back destination. + +```typescript +{ + name: "Article", + route: { + path: "/articles/:articleId", + defaultHistory: (_params, { initialContext }) => [ + { + activityName: "Home", + activityParams: { + locale: initialContext.locale, + }, + }, + ], + }, +} +``` + +The second argument contains the Stack's `initialContext`. It is available in +`@stackflow/plugin-history-sync@1.12.0` and later. Return +`{ entries, skipDefaultHistorySetupTransition: true }` to create the default +history without the staged setup transition. + +## Server-side rendering + +The server cannot read `window.location`. Pass the request path through the +Stack's `initialContext` so the server and the first client render select the +same route. + +```tsx + +``` + +The same `initialContext` is passed to `fallbackActivity` and each +`defaultHistory` callback. + +## Behavior + +### Stack and browser history + +- `push()` and `stepPush()` add browser history entries after the navigation is + committed. +- `replace()` updates the current browser entry. +- `pop()` and `stepPop()` move back through browser history. +- Browser back and forward actions are translated into Stackflow navigation. +- Set `useHash: true` to store the Stackflow path in `window.location.hash`. + +Each Activity step occupies one browser entry. Popping an Activity with +multiple steps can therefore move across more than one browser entry. + +### Prevented navigation + +Version 2.0.0 synchronizes the browser from committed Stackflow state. If a +plugin prevents a programmatic navigation, the URL is left unchanged. If a +plugin prevents browser back or forward navigation, the browser is reconciled +back to the committed Stackflow screen. + +This makes the plugin compatible with `preventDefault`-based plugins such as +`@stackflow/plugin-blocker` without requiring additional configuration. + +### Snapshot restoration + +When `@stackflow/core@3` loads a snapshot, version 2.0.0 preserves the +snapshot's navigation events. It does not replace them with a new initial stack +derived from the current URL, browser state, `defaultHistory`, or +`fallbackActivity`. + +URL-based initialization still applies when Stackflow creates a fresh stack. +After a snapshot is loaded, the plugin continues synchronizing browser history +from committed changes to the restored stack. + +## Migrating from 1.12 to 2.0 + +### 1. Upgrade Core and the plugin together + +```bash +yarn add @stackflow/core@^3.0.0 @stackflow/plugin-history-sync@^2.0.0 +``` + +Version 2 uses the Core 3 initialization contract to distinguish a fresh stack +from a snapshot load. Core 2 is not supported. + +### 2. Verify snapshot initialization + +In version 1.12, history sync could replace snapshot events during +initialization with events reconstructed from the current URL. Version 2 keeps +the restored snapshot history instead. + +If the application provides snapshots, verify the following flows after the +upgrade: + +- restore a snapshot whose active Activity differs from the current URL; +- use browser back and forward across restored Activities and steps; +- reload or replace the JavaScript runtime, then continue navigation; +- handle a rejected or invalid snapshot according to the application's + snapshot-provider policy, including fresh URL-based initialization when the + provider recovers with a fresh stack. + +### 3. Verify navigation blockers + +Version 2 replaces the previous pre-navigation browser mutations with +post-commit reconciliation. Test both programmatic and browser back/forward +navigation while a blocker prevents the action, and after the user chooses to +proceed. + +### 4. Keep request-specific default history + +No callback change is required when upgrading from 1.12. Existing +`defaultHistory(params)` callbacks remain valid. Callbacks can optionally read +the second `{ initialContext }` argument introduced in 1.12. + +## Release notes + +### 2.0.0 + +- Requires `@stackflow/core@3`. +- Preserves snapshot navigation history during initialization. +- Synchronizes browser history from committed Stackflow state so prevented + navigation no longer leaves the browser and stack out of sync. + +### 1.12.0 + +- Passes `{ initialContext }` as the second argument to route + `defaultHistory` callbacks. +- Was released with `@stackflow/core@3` as its Core peer dependency. + +## API + +### `historySyncPlugin()` + +```typescript +function historySyncPlugin(options: { + config: Config; + fallbackActivity: (args: { initialContext: any }) => ActivityName; + useHash?: boolean; + history?: History; + urlPatternOptions?: UrlPatternOptions; +}): StackflowReactPlugin; +``` + +For applications that do not use `defineConfig()`, `routes` can be supplied in +place of `config`. + +| Option | Description | +| --- | --- | +| `config` | A Stackflow config whose Activity definitions contain routes. | +| `routes` | A map from Activity names to routes. Mutually exclusive with `config`. | +| `fallbackActivity` | Selects the initial Activity when no route matches the current URL. | +| `useHash` | Uses the hash portion of the URL. Defaults to `false`. | +| `history` | Supplies a custom `history` instance. Defaults to browser history in the browser and memory history on the server. | +| `urlPatternOptions` | Customizes the route pattern syntax. | + +### Route object + +```typescript +type Route = { + path: string; + decode?: (params: Record) => ActivityParams; + encode?: (params: ActivityParams) => Record; + defaultHistory?: ( + params: Record, + args: { initialContext: any }, + ) => + | HistoryEntry[] + | { + entries: HistoryEntry[]; + skipDefaultHistorySetupTransition?: boolean; + }; +}; +``` + +A route can be a path string, an array of path strings, a route object, or an +array of route objects. + +### Hooks + +- `useRoutes()` returns the normalized Activity routes registered by the + plugin. +- `useIsActivatedActivity()` reports whether the calling Activity is active + after any default-history setup navigation has completed. +- `useHistoryTick()` exposes the history task queue for integrations that need + to coordinate work with browser history updates. + +### Utilities and types + +The package also exports `makeTemplate`, `UrlPatternOptions`, `Route`, and +`RouteLike` for integrations that need to use the same route parsing and URL +generation behavior. From 3f1f1740be13a8e1be5e8753b2c6e683362fdb3c Mon Sep 17 00:00:00 2001 From: ENvironmentSet Date: Tue, 4 Aug 2026 16:44:09 +0900 Subject: [PATCH 2/2] chore: mark FEP-2669 as documentation only --- .changeset/fep-2669-history-sync-docs.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changeset/fep-2669-history-sync-docs.md diff --git a/.changeset/fep-2669-history-sync-docs.md b/.changeset/fep-2669-history-sync-docs.md new file mode 100644 index 000000000..a845151cc --- /dev/null +++ b/.changeset/fep-2669-history-sync-docs.md @@ -0,0 +1,2 @@ +--- +---