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

alpinejs-persist-extended

v2.0.0

Published

Extends the official Alpine JS `$persist` plugin with expiring values and imperative read, write, delete and clear helpers πŸ“¦

Readme

Alpine JS Persist Extended

Extends the official Alpine JS $persist plugin with the things it deliberately leaves out: values that expire, and imperative access to persisted keys you don't own.

$persist is declarative only β€” the sole way to touch a key is to declare a reactive property bound to it. That leaves two gaps this plugin fills:

  • Nothing expires. "Hide this banner for 7 days" or "cache this response for an hour" means hand-rolling timestamps.
  • A "clear my saved data" button can't reset keys owned by components that aren't currently on the page, because there's no property to assign to.

Benefits

  • ⏳ $persistExpire: $persist with a TTL β€” '7d', '30m', '45s' or raw milliseconds
  • πŸ“Œ $persistGet: read a persisted key without declaring it in x-data
  • ✏️ $persistSet: write a persisted key without declaring it in x-data
  • πŸ—‘οΈ $persistDelete: remove one key, with an event that says which
  • 🧹 $persistClear: sweep every Alpine-persisted key, optionally by prefix
  • 🀝 Byte-compatible with $persist's storage format, including .as() and .using()
  • πŸͺΆ ~1.5KB gzipped, zero dependencies beyond Alpine JS

Install

CDN

<script
  defer
  src="https://unpkg.com/alpinejs-persist-extended@latest/dist/cdn.min.js"
></script>

<script
  defer
  src="https://unpkg.com/@alpinejs/persist@latest/dist/cdn.min.js"
></script>

<script defer src="https://unpkg.com/alpinejs@latest/dist/cdn.min.js"></script>

Package Manager

pnpm add -D alpinejs-persist-extended

yarn add -D alpinejs-persist-extended

npm install -D alpinejs-persist-extended
import Alpine from 'alpinejs'
import persist from '@alpinejs/persist'
import persistExtended from 'alpinejs-persist-extended'

Alpine.plugin(persist)
Alpine.plugin(persistExtended)

window.Alpine = Alpine

Alpine.start()

@alpinejs/persist is only required if you use $persist alongside this plugin. $persistExpire and the imperative helpers work on their own.

API Reference

$persistExpire(value, ttl)

Works exactly like $persist, but the stored value is discarded once ttl elapses, falling back to the initial value.

<div x-data="{ dismissed: $persistExpire(false, '7d') }" x-show="!dismissed">
  <p>We use cookies, obviously.</p>

  <button type="button" @click="dismissed = true">Dismiss for a week</button>
</div>

ttl takes milliseconds, or a duration string β€” ms, s, m, h, d, w:

$persistExpire([], 900000)
$persistExpire([], '15m')

Expiry is evaluated when the value is read β€” on page load, or on a $persistGet β€” never on a timer. A value that expires while the page is open stays in memory until the next read. An invalid duration throws immediately rather than silently persisting forever.

The window is fixed from the first write by default. .sliding() restarts it on every change instead, which is what you want for "keep this while the user is active":

$persistExpire({}, '30m').sliding()

.as() and .using() behave as they do in the official plugin, and chain in any order:

$persistExpire('dark', '1d').as('theme').using(sessionStorage)

Alpine.$persistExpire is available for stores, mirroring Alpine.$persist:

Alpine.store('cart', { items: Alpine.$persistExpire([], '7d') })

$persistGet(key, fallback?, options?)

Reads a persisted key and returns the parsed value, or fallback when the key is missing or expired.

<button type="button" @click="alert($persistGet('name', 'nobody'))">
  Show persisted name
</button>

key is the property name as written in x-data β€” the _x_ prefix is added for you. A .as() alias or any other raw storage key also resolves, so both of these find the same value:

$persistGet('theme') // property declared as $persist('dark')
$persistGet('my_theme') // property declared as $persist('dark').as('my_theme')

Values are JSON.parsed, matching how $persist wrote them. A key that isn't valid JSON β€” one written by something other than Alpine β€” comes back as its raw string rather than throwing.

Reads are one-shot and not reactive. Use it in event handlers, not in x-text or x-show, where it won't update when storage changes. For shared reactive state, put $persist in an Alpine.store instead.

$persistSet(key, value, options?)

Writes a persisted key, in the same format $persist uses, with an optional TTL.

<button type="button" @click="$persistSet('seenTour', true, { ttl: '30d' })">
  Don't show again
</button>

Dispatches persist:set with { key, value }.

[!IMPORTANT] If a mounted component owns that key via $persist, this does not update its in-memory value, and that component will overwrite you on its next change. Use it for keys nothing on the current page owns β€” seeding a flag for the next page, for example. To change state a component owns, assign to the property.

$persistDelete(key, options?)

Removes a persisted key and its expiry. Returns whether the key existed, and dispatches persist:delete with { key, existed }.

<div x-data="{ name: $persist('Rob Brydon') }">
  <h2 x-text="name"></h2>

  <button type="button" @click="$persistDelete('name')">Reset name</button>
</div>

The event bubbles, so listen on an ancestor or with .window, and use $event.detail.key to tell keys apart:

<div @persist:delete.window="if ($event.detail.key === '_x_name') name = ''"></div>

That listener is load-bearing: deleting a key can't reset the live property of a component that owns it, so a mounted $persist property keeps its value β€” and rewrites it to storage on its next change β€” until you clear it yourself.

$persistClear(options?)

Removes every key in Alpine's _x_ namespace. Returns the removed keys and dispatches persist:clear with { keys }.

<button type="button" @click="$persistClear()">Clear saved data</button>

Unlike localStorage.clear(), this leaves keys your app stores outside that namespace alone. Scope it further with a prefix:

$persistClear({ prefix: 'cart' }) // clears _x_cartItems, _x_cartTotal, ...

Keys renamed with .as() fall outside the _x_ namespace and are not swept β€” remove those with $persistDelete.

Options

$persistGet, $persistSet, $persistDelete and $persistClear all take a trailing options object:

| Option | Applies to | Default | Description | | --------- | ----------------- | -------------- | ------------------------------------ | | storage | all | localStorage | Any Storage, e.g. sessionStorage | | ttl | $persistSet | none | Duration string or milliseconds | | prefix | $persistClear | '' | Restrict the sweep within _x_ |

Each helper is also exposed on the Alpine object for use outside markup β€” Alpine.persistGet, Alpine.persistSet, Alpine.persistDelete, Alpine.persistClear. Events from those dispatch off the document root, so .window listeners still fire.

Storage format

Values are stored exactly as $persist stores them: JSON.stringify(value) under _x_<property>. Expiry lives in a sidecar key, _x_<property>__x_expires, holding an epoch milliseconds timestamp. Nothing wraps the value, so a key can be moved between $persist and $persistExpire without a migration, and $persistGet reads keys written by either.

When localStorage is unavailable β€” Safari private mode, blocked cookies β€” the plugin warns once and falls back to in-memory storage for that page load rather than throwing.

Breaking Changes in 2.0.0

  • $persistGet now parses values. It previously returned the raw string, so $persist('Rob Brydon') read back as "Rob Brydon" including the quote characters, and objects came back as JSON text. If you were working around that with JSON.parse($persistGet('key')), drop the JSON.parse.
  • $persistGet and $persistDelete resolve .as() aliases and other raw keys, where before they only ever looked at _x_<key>.
  • $persistGet returns undefined, not null, for a missing key, and takes a fallback argument.
  • $persistDelete returns a boolean and its persist:delete event now carries detail: { key, existed }. The event is no longer cancelable; nothing read that flag.
  • Both helpers honour expiry, sweeping and skipping an expired key.
  • dist/esm.min.js is gone, replaced by dist/module.mjs and dist/module.cjs behind an exports map β€” 1.2.0 declared only module, so neither require() nor Node ESM import resolved. Package-name imports are unaffected; update any deep imports of the old path. dist/cdn.min.js is unchanged, so CDN users need no changes.