@rune-hub/utils
v1.1.0
Published
Utils for rune-hub
Maintainers
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.
Index
[ Install ]
[ Hooks ] persistent • persistentBool • persistentNum • persistentJSON
[ 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/utilsHooks
🏠︎ / Hooks ↑ ↓
persistent • persistentBool • persistentNum • persistentJSON
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')) // nullWhen 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 ↑
- Creator: Mike Lysikov
- Source Code: GitHub
- Repository: npm • npmx
- Core: rune-hub
Contributions are welcome! Please feel free to submit issues and pull requests.
