npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@thrylm/baqk

v0.1.4

Published

Smart back navigation with state preservation for React apps

Downloads

617

Readme

baqk

Smart back navigation with state preservation for React apps.

npm npm bundle size license

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.

~3.5 kB gzipped (core + hook). Each adapter adds ~500 B.

Quick Start

// app.tsx — wrap your app with the adapter
import { BaqkAdapter } from "@thrylm/baqk/adapters/react-router";

function App() {
  return (
    <BaqkAdapter>
      <Routes>{/* ... */}</Routes>
    </BaqkAdapter>
  );
}
// products.tsx — listing page
import { useBaqk, useTrailClick } from "@thrylm/baqk";
import { Link } from "react-router-dom";

function ProductList() {
  const { restoredState, saveState } = useBaqk<{ filters: Filters }>();
  const trailClick = useTrailClick("Products");

  // Restore filters synchronously — no useEffect
  const [filters, setFilters] = useState(
    () => restoredState?.filters ?? defaultFilters,
  );

  // Save state whenever filters change
  useEffect(() => { saveState({ filters }); }, [filters]);

  return products.map((p) => (
    <Link to={`/products/${p.id}`} onClick={trailClick}>
      {p.name}
    </Link>
  ));
}
// product-detail.tsx — detail page
import { useBaqk } from "@thrylm/baqk";

function ProductDetail() {
  const { goBack, previousEntry } = useBaqk({
    fallbackPath: "/products",
  });

  return (
    <div>
      <button onClick={() => goBack()}>
        {previousEntry ? `← ${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

useTrailClick(label?)

Returns an onClick handler that pushes a trail entry without navigating. Attach it to same-tab internal <Link>/anchor navigations — the link handles navigation natively, no preventDefault needed.

import { useTrailClick } from "@thrylm/baqk";

function ProductList() {
  const trailClick = useTrailClick("Products");

  return products.map((p) => (
    <Link to={`/products/${p.id}`} onClick={trailClick}>
      {p.name}
    </Link>
  ));
}

Behavior:

  • Saves scroll position and pushes the current page onto the trail
  • Skips on modifier keys (meta, ctrl, shift, alt), middle-click, or defaultPrevented
  • Skips new-tab, download, external, and hash-only anchor clicks
  • Captures path at click time via getCurrentPath() (reads window.location, compatible with nuqs/shallow updates)
  • Does NOT call preventDefault() or navigate — the underlying link handles that
  • Shares the same navId as useBaqk() (both use ensureNavId which is idempotent)

useBaqk<T>(options?)

Options (BaqkOptions)

| Option | Type | Default | Description | |--------|------|---------|-------------| | 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>)

| Property | Type | Description | |----------|------|-------------| | goBack | (fallbackPath?) => void | Pop the trail and navigate back, or use fallback | | previousEntry | TrailEntry \| null | The most recent trail entry (the page you'd go back to) | | saveState | (state: T) => void | Save state for the current page | | restoredState | T \| null | Synchronously available saved state (lazy ref pattern) | | wasRestored | boolean | Whether state was restored for this page | | clear | () => void | Clear the trail and all associated state |

TrailEntry

interface TrailEntry {
  path: string;
  navId: string;
  label?: string;
  timestamp: number;
}

BaqkAdapterProps (router-specific adapters)

| Prop | Type | Description | |------|------|-------------| | 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:

| Prop | Type | Description | |------|------|-------------| | 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 useTrailClick fires, 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 a RouterAdapter: { getCurrentPath, navigate, getHistoryState, replaceHistoryState }
  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