Summary
Add a framework-agnostic middleware for synchronizing selected state with the URI/query/hash.
This would allow view state such as search terms, filters, sorting, pagination, selected tabs, or wizard steps to be restored on refresh and shared via URL.
Background
The current middleware system already supports composition through pipe(...), and existing middleware such as persist(...) follows this pattern:
- Read an initial persisted value.
- Merge or apply it to the store.
- Use
store.pushMiddleware(...) to write updates after state changes.
Current persist(...) support is storage-oriented only:
localStorage
sessionStorage
cookie
There is no URI/query/hash-backed state synchronization yet.
Proposed design
Introduce a separate middleware, tentatively named uriPersist(...), instead of extending persist(...).
const store = pipe(
initialState,
uriPersist({
key: 'products',
mode: 'query',
history: 'replace',
source: browserUrlSource(),
partialize: (state) => ({
q: state.q,
page: state.page,
sort: state.sort,
}),
merge: (state, uriState) => ({
...state,
...uriState,
}),
}),
);
Architecture
Use three layers:
-
Core uriPersist middleware
- Depends only on
Store<T> and an abstract URL source.
- Does not import React, Next.js, Vue, Vue Router, or browser globals directly.
-
UrlSource abstraction
- Encapsulates URL read/write/subscribe behavior.
- Allows different routing environments to provide their own URL integration.
type UrlSource = {
read: () => URL | null;
write: (url: URL, mode: 'replace' | 'push') => void;
subscribe?: (listener: () => void) => () => void;
writable?: () => boolean;
};
-
Sources/adapters
browserUrlSource() for general browser usage.
- Optional router-specific adapters later:
nextUrlSource(...)
vueRouterUrlSource(...)
reactRouterUrlSource(...)
Core options
Potential option shape:
type UriPersistOptions<T, P> = {
key: string;
mode?: 'query' | 'hash';
history?: 'replace' | 'push';
source?: UrlSource;
partialize?: (state: T) => P;
merge?: (state: T, uriState: P) => T;
serialize?: (state: P) => string;
deserialize?: (value: string) => P;
version?: number;
migrate?: Array<(state: unknown) => unknown>;
equals?: (a: P, b: P) => boolean;
};
Default behavior
- Default
mode: query
- Default
history: replace
- Default source:
browserUrlSource()
- Default serialization: JSON + URL encoding
- URL state should override or merge into initial state during client initialization.
- Only selected state should be persisted via
partialize(...).
SSR and framework compatibility
The core middleware should be SSR-safe:
- Do not access
window at module initialization time.
browserUrlSource() should check typeof window !== 'undefined' lazily.
- On the server, URL read/write should be unavailable or no-op.
Framework support should come from the UrlSource boundary:
- React, Svelte, Solid, Angular, and vanilla usage can use
browserUrlSource().
- Next.js can use either native History API support or a Next-specific adapter.
- Vue Router apps should prefer a router adapter using
router.push / router.replace to avoid router state drift.
Feedback-loop prevention
The implementation must prevent loops between store updates and URL updates:
store change -> URL write -> URL listener -> store change -> URL write -> ...
Possible safeguards:
- Track
isWritingUrl.
- Track the last serialized URI value.
- Skip URL-to-store sync if the incoming value equals the last written value.
- Support custom
equals(...) comparison.
History policy
Default to replace because most URL-backed view state should not create a new browser history entry on every keystroke or filter change.
Allow push for states where browser back/forward should navigate between state changes, such as wizard steps or tab navigation.
Security and size constraints
Document that URI-backed state must not contain:
- access tokens
- secrets
- personal information
- large objects
- unstable or non-serializable data
This feature should be positioned for small, shareable view state rather than general persistence.
Suggested file structure
src/middleware/uriPersist/
index.ts
types.ts
serializer.ts
sources/
browser.ts
utils.ts
Potential exports:
export { uriPersist } from './uriPersist';
export { browserUrlSource } from './uriPersist/sources/browser';
Router adapters can be added later as separate entry points to avoid adding framework/router dependencies to the middleware core.
Acceptance criteria
Summary
Add a framework-agnostic middleware for synchronizing selected state with the URI/query/hash.
This would allow view state such as search terms, filters, sorting, pagination, selected tabs, or wizard steps to be restored on refresh and shared via URL.
Background
The current middleware system already supports composition through
pipe(...), and existing middleware such aspersist(...)follows this pattern:store.pushMiddleware(...)to write updates after state changes.Current
persist(...)support is storage-oriented only:localStoragesessionStoragecookieThere is no URI/query/hash-backed state synchronization yet.
Proposed design
Introduce a separate middleware, tentatively named
uriPersist(...), instead of extendingpersist(...).Architecture
Use three layers:
Core
uriPersistmiddlewareStore<T>and an abstract URL source.UrlSourceabstractionSources/adapters
browserUrlSource()for general browser usage.nextUrlSource(...)vueRouterUrlSource(...)reactRouterUrlSource(...)Core options
Potential option shape:
Default behavior
mode:queryhistory:replacebrowserUrlSource()partialize(...).SSR and framework compatibility
The core middleware should be SSR-safe:
windowat module initialization time.browserUrlSource()should checktypeof window !== 'undefined'lazily.Framework support should come from the
UrlSourceboundary:browserUrlSource().router.push/router.replaceto avoid router state drift.Feedback-loop prevention
The implementation must prevent loops between store updates and URL updates:
Possible safeguards:
isWritingUrl.equals(...)comparison.History policy
Default to
replacebecause most URL-backed view state should not create a new browser history entry on every keystroke or filter change.Allow
pushfor states where browser back/forward should navigate between state changes, such as wizard steps or tab navigation.Security and size constraints
Document that URI-backed state must not contain:
This feature should be positioned for small, shareable view state rather than general persistence.
Suggested file structure
src/middleware/uriPersist/ index.ts types.ts serializer.ts sources/ browser.ts utils.tsPotential exports:
Router adapters can be added later as separate entry points to avoid adding framework/router dependencies to the middleware core.
Acceptance criteria
uriPersist(...)works with the existingpipe(...)middleware composition API.replaceby default.pushhistory mode is supported.windowaccess.