baqk
Smart back navigation with state preservation for React apps.

The Problem
User filters a list, clicks into a detail page, hits back — filters are gone. history.back() can't carry state, and sessionStorage alone doesn't know which page to restore. baqk solves this with a hybrid navId + sessionStorage approach that preserves state, scroll position, and navigation context across any number of levels.
Install
npm install @thrylm/baqk
ESM-only. Requires react >= 18.
Quick Start
import { BaqkAdapter } from "@thrylm/baqk/adapters/react-router";
function App() {
return (
<BaqkAdapter>
<Routes>{/* ... */}</Routes>
</BaqkAdapter>
);
}
import { useBaqk } from "@thrylm/baqk";
function ProductList() {
const { restoredState, saveState, navigateWithTrail } =
useBaqk<{ filters: Filters }>();
const [filters, setFilters] = useState(
() => restoredState?.filters ?? defaultFilters,
);
function openProduct(id: string) {
saveState({ filters });
navigateWithTrail(`/products/${id}`, { label: "Products" });
}
return <FilteredList filters={filters} onSelect={openProduct} />;
}
import { useBaqk } from "@thrylm/baqk";
function ProductDetail() {
const { goBack, hasTrail, previousEntry } = useBaqk({
fallbackPath: "/products",
});
return (
<div>
<button onClick={() => goBack()}>
{hasTrail ? `← ${previousEntry?.label}` : "← Products"}
</button>
{/* ... */}
</div>
);
}
Adapters
React Router
import { BaqkAdapter } from "@thrylm/baqk/adapters/react-router";
<BaqkAdapter>
<RouterProvider router={router} />
</BaqkAdapter>
Next.js App Router
import { BaqkAdapter } from "@thrylm/baqk/adapters/next";
<BaqkAdapter>
{children}
</BaqkAdapter>
TanStack Router
import { BaqkAdapter } from "@thrylm/baqk/adapters/tanstack";
<BaqkAdapter>
<RouterProvider router={router} />
</BaqkAdapter>
Generic (any router)
import { BaqkAdapter } from "@thrylm/baqk";
<BaqkAdapter
navigate={(path, options) =>
options?.replace ? myRouter.replace(path) : myRouter.push(path)
}
getCurrentPath={() => window.location.pathname + window.location.search}
>
{children}
</BaqkAdapter>
All router-specific adapters accept optional sessionKey and storage props. The generic adapter additionally requires navigate and getCurrentPath.
API Reference
useBaqk<T>(options?)
Options (BaqkOptions)
fallbackPath | string | undefined | Path to navigate to when goBack() is called with no trail |
autoSaveScroll | boolean | true | Automatically save/restore scroll position |
Return value (BaqkResult<T>)
restoredState | T | null | Synchronously available saved state (lazy ref pattern) |
wasRestored | boolean | Whether state was restored for this page |
saveState | (state: T) => void | Save state for the current page |
restoreState | () => T | null | Manually restore state (usually not needed — use restoredState) |
navigateWithTrail | (path, opts?) => void | Navigate to path, pushing the current page onto the trail |
goBack | (fallbackPath?) => void | Pop the trail and navigate back, or use fallback |
hasTrail | boolean | Whether there are entries in the trail |
previousEntry | TrailEntry | null | The most recent trail entry (the page you'd go back to) |
trail | readonly TrailEntry[] | The full trail stack |
clearAll | () => void | Clear the trail and all associated state |
TrailEntry
interface TrailEntry {
path: string;
navId: string;
label?: string;
timestamp: number;
}
navigateWithTrail options
label | string | Label for the breadcrumb (e.g. "Products") |
state | T | State to save for the current page before navigating |
BaqkAdapterProps (router-specific adapters)
children | ReactNode | — |
sessionKey | string? | Namespace for storage isolation (e.g. user ID) |
storage | StorageAdapter? | Custom storage backend (defaults to sessionStorage) |
GenericBaqkAdapterProps (generic adapter)
Extends BaqkAdapterProps with:
navigate | (path: string, options?: { replace?: boolean }) => void | Navigation function |
getCurrentPath | () => string | Returns current path + search params |
How It Works
- Each page visit gets a unique navId stamped into
history.state
- When you call
navigateWithTrail, the current page's path + navId are pushed onto a trail stack in sessionStorage
- State and scroll position are keyed by
sessionKey:navId, so they survive navigations
goBack pops the trail, navigates to the previous path, and re-stamps the navId — triggering automatic state + scroll restoration
restoredState is computed synchronously via a lazy ref (no useEffect, no flash of default state)
Advanced
Session key
Use sessionKey to isolate trails per user or session:
<BaqkAdapter sessionKey={user.id}>
Custom storage
import { createMemoryStorage } from "@thrylm/baqk";
<BaqkAdapter storage={createMemoryStorage()}>
createBaqkAdapter factory
Build an adapter for any router:
import { createBaqkAdapter } from "@thrylm/baqk";
const MyBaqkAdapter = createBaqkAdapter(() => {
return useMyRouter();
});
Limits
- Trail stack: max 50 entries (oldest evicted with their state)
- State size: max 100 KB per entry (oversized state is silently dropped with a console warning)
License
MIT