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

@rune-hub/utils

v1.1.0

Published

Utils for rune-hub

Readme

@rune-hub/utils provides utility hooks for rune-hub that simplify common patterns like persistent state management. The library includes specialized hooks for browser storage synchronization with automatic encoding/decoding, cross-tab updates, and type-safe interfaces.

stars watchers

Index

[ Install ]
[ Hooks ] persistentpersistentBoolpersistentNumpersistentJSON
[ Links ]

Install

🏠︎ / Install

Requires rune-hub 1.0+ as a peer dependency.

These utilities are designed to work with rune-hub's reactive state management system. Make sure to install both packages:

npm i rune-hub @rune-hub/utils

Hooks

🏠︎ / Hooks

persistentpersistentBoolpersistentNumpersistentJSON

persistent

🏠︎ / Hooks / persistent

Creates a persistent Rune that synchronizes its state with browser storage (localStorage by default).

The Rune automatically loads the initial value from storage and saves changes back. It also listens to storage and pageshow events to sync state across tabs and page navigations.

The simplest form accepts only a key parameter and works directly with string values. The Rune returns string | null. This approach provides direct one-to-one mapping between your state and storage: if there's no value in storage, you get null, otherwise you get the exact string stored. When you set the value to null, it clears the storage entry completely.

import { get, set } from 'rune-hub'
import { persistent } from '@rune-hub/utils'

const state = () => persistent('state')

console.log(get(state)) // null initially

set(state, 'foo')
console.log(get(state)) // 'foo'

set(state, null)
console.log(get(state)) // null (storage is clean)

When you provide an initial value, it serves as a fallback when storage is empty. Note that passing null as the initial value works the same as omitting it entirely.

When you provide a string as the initial value, the Rune becomes strictly typed and returns string (not nullable). In this mode, you cannot clear the state by setting it to the initial value — the storage will keep the entry with that value. This is useful when you always want a valid string state without null checks.

const lang = () => persistent('lang', 'en')

console.log(get(lang)) // 'en' initially

set(lang, 'ru')
console.log(get(lang)) // 'ru'

set(lang, 'en')
console.log(get(lang)) // 'en' (storage is not clean)

This approach also allows you to safely change the default value in the future. For example, if the default theme was initially 'dark', a user changed it to 'light', then back to 'dark'. Later, developers change the default to 'auto' — users who manually selected 'dark' will keep their choice because it's stored. Only users who never changed this setting (no value in storage) will get the new 'auto' default.

You can specify allowed state variants using generics to get type safety and autocomplete:

type Theme = 'dark' | 'light' | 'auto'
const theme = () => persistent<Theme>('search', 'auto')

By default, persistent uses localStorage, which persists data permanently across browser sessions and tabs. You can use sessionStorage instead for temporary data that only exists within the current tab and is cleared when the tab closes. Also, you can use simple/proxy object as storage, the storage changes by set or delete value (storage[key] = value or delete storage[key]).

const chatDraft = () => persistent('chatDraft', '', {
  storage: sessionStorage
})

When your state type is not string | null, you must provide encode and decode functions to convert between your type and string storage format.

The encode function converts your state value to a string (or null to clear storage), while decode converts the stored string back to your state type.

const isEnabled = () => persistent('isEnabled', false, {
  decode: v => v === '+',
  encode: v => v ? '+' : '-'
})

set(isEnabled, true)
console.log(localStorage.getItem('isEnabled')) // '+'

For string | null or string state types, you can optionally provide encode and decode to override the default conversion logic. For example, you can make the state clear from storage when set back to the initial value:

const lang = () => persistent('lang', '', {
  encode: v => v ? v : null
})

set(lang, 'en')
console.log(localStorage.getItem('lang')) // 'en'

set(lang, '')
console.log(localStorage.getItem('lang')) // null (storage is clean)

persistentBool

🏠︎ / Hooks / persistentBool

Creates a persistent Rune for boolean values with convenient string encoding (+ for true, - for false by default). Automatically loads the initial value from storage and saves changes back to it.

The simplest form accepts only a key parameter and works with nullable boolean values. The Rune returns boolean | null. If there's no value in storage, you get null, otherwise you get the decoded boolean value. When you set the value to null, it clears the storage entry completely.

import { get, set } from 'rune-hub'
import { persistentBool } from '@rune-hub/utils'

const consent = () => persistentBool('consent')

console.log(get(consent)) // null initially

set(consent, true)
console.log(get(consent)) // true
console.log(localStorage.getItem('consent')) // '+' (encoded)

set(consent, null)
console.log(get(consent)) // null (storage is clean)

When you provide an initial value, it serves as a fallback when storage is empty. When you provide a boolean as the initial value, the Rune becomes strictly typed and returns boolean (not nullable). In this mode, you cannot clear the state by setting it to null — the storage will keep the entry with the encoded value. This is useful when you always want a valid boolean state without null checks.

const isDarkMode = () => persistentBool('darkMode', false)

console.log(get(isDarkMode)) // false initially

set(isDarkMode, true)
console.log(localStorage.getItem('darkMode')) // '+' (true encoded)

set(isDarkMode, false)
console.log(localStorage.getItem('darkMode')) // '-' (false encoded)

You can customize the encoding by specifying what strings represent true and false:

const isAccepted = () => persistentBool('accepted', false, {
  true: 'yes',
  false: 'no'
})

set(isAccepted, true)
console.log(localStorage.getItem('accepted')) // 'yes'

By default, persistentBool uses localStorage for permanent persistence across browser sessions. You can use sessionStorage instead for temporary data that only exists within the current tab and is cleared when the tab closes. You can also use a simple/proxy object as storage.

const isExpanded = () => persistentBool('expanded', false, {
  storage: sessionStorage
})

persistentNum

🏠︎ / Hooks / persistentNum

Creates a persistent Rune for numeric values with automatic number parsing and string conversion. Automatically loads the initial value from storage and saves changes back to it.

The simplest form accepts only a key parameter and works with nullable numeric values. The Rune returns number | null.

If there's no value in storage, you get null, otherwise you get the parsed number. When you set the value to null, it clears the storage entry completely.

import { get, set } from 'rune-hub'
import { persistentNum } from '@rune-hub/utils'

const score = () => persistentNum('score')

console.log(get(score)) // null initially

set(score, 100)
console.log(get(score)) // 100
console.log(localStorage.getItem('score')) // '100'

set(score, null)
console.log(get(score)) // null (storage is clean)
console.log(localStorage.getItem('score')) // null

When you provide an initial value, it serves as a fallback when storage is empty. When you provide a number as the initial value, the Rune becomes strictly typed and returns number (not nullable). In this mode, you cannot clear the state by setting it to null — the storage will keep the entry with that value. This is useful when you always want a valid numeric state without null checks.

const count = () => persistentNum('count', 0)

console.log(get(count)) // 0 initially

set(count, 42)
console.log(localStorage.getItem('count')) // '42'

set(count, 0)
console.log(localStorage.getItem('count')) // '0' (storage is not clean)

By default, persistentNum uses localStorage for permanent persistence across browser sessions. You can use sessionStorage instead for temporary data that only exists within the current tab and is cleared when the tab closes. You can also use a simple/proxy object as storage.

const tempCounter = () => persistentNum('counter', 0, {
  storage: sessionStorage
})

persistentJSON

🏠︎ / Hooks / persistentJSON

Creates a persistent Rune with automatic JSON serialization/deserialization for complex data structures. Automatically loads the initial value from storage and saves changes back to it.

The simplest form accepts only a key parameter and works with nullable values. The Rune returns T | null where T is your data type.

If there's no value in storage, you get null, otherwise you get the parsed object. When you set the value to null, it stores the string "null" in storage (unlike other persistent hooks).

import { get, set } from 'rune-hub'
import { persistentJSON } from '@rune-hub/utils'

interface User {
  name: string
  age: number
}

const user = () => persistentJSON<User>('user')

console.log(get(user)) // null initially

set(user, { name: 'John', age: 30 })
console.log(get(user)) // { name: 'John', age: 30 }
console.log(localStorage.getItem('user')) // '{"name":"John","age":30}'

set(user, null)
console.log(localStorage.getItem('user')) // '"null"' (stored as string)

When you provide an initial value, it serves as a fallback when storage is empty. When you provide an object as the initial value, the Rune becomes strictly typed and returns your type T (not nullable). In this mode, you cannot clear the state by setting it to null — the storage will keep the entry with the encoded value. This is useful when you always want a valid object state without null checks.

import { get, set } from 'rune-hub'
import { persistentJSON } from '@rune-hub/utils'

const user = () => persistentJSON('user', { name: 'John', age: 30 })

console.log(get(user)) // { name: 'John', age: 30 } initially

set(user, { name: 'Jane', age: 25 })
console.log(localStorage.getItem('user')) // '{"name":"Jane","age":25}'

set(user, { name: 'John', age: 30 })
console.log(localStorage.getItem('user')) // '{"name":"John","age":30}' (storage is not clean)

You can also use persistentJSON with arrays:

const items = () => persistentJSON<string[]>('items', [])

set(items, ['apple', 'banana', 'orange'])
console.log(get(items)) // ['apple', 'banana', 'orange']
console.log(localStorage.getItem('items')) // '["apple","banana","orange"]'

By default, persistentJSON uses localStorage for permanent persistence across browser sessions. You can use sessionStorage instead for temporary data that only exists within the current tab and is cleared when the tab closes. You can also use a simple/proxy object as storage.

const tempData = () => persistentJSON<any>('data', null, {
  storage: sessionStorage
})

Links

🏠︎ / Links

Contributions are welcome! Please feel free to submit issues and pull requests.

issues pulls