State API
Complete reference for Nuestate's state management API. For an overview and introduction, see the Nuestate documentation.
state object
The main state
object is a proxy that handles all reading and writing of application state. Import it from anywhere in your application:
import { state } from 'state'
Note: Nuekit automatically maps 'state'
to /@nue/state.js
via import maps, so you don't need to specify the full path.
Or use it directly from a CDN:
<script type="module">
import { state } from '//esm.sh/nuestate'
</script>
Reading state
Access any property directly:
console.log(state.view) // current value or undefined
console.log(state.search) // current value or undefined
console.log(state.user) // current value or undefined
State is automatically populated from the current URL and browser storage when the application starts.
Writing state
Set any property directly:
state.view = 'users'
state.search = 'john'
state.user = { name: 'Alice', id: 123 }
State changes automatically trigger URL updates and storage persistence based on your configuration.
state.setup()
Configure where different pieces of state should be stored and how routing should work.
state.setup(config)
Configuration options
route - Route pattern with parameters
state.setup({
route: '/app/:section/:id'
})
// Setting route parameters updates the URL path
state.section = 'products' // URL: /app/products
state.id = '123' // URL: /app/products/123
query - Array of properties that go in URL search parameters
state.setup({
query: ['search', 'filter', 'page']
})
// Setting query properties updates the URL search
state.search = 'shoes' // URL: ?search=shoes
state.filter = 'active' // URL: ?search=shoes&filter=active
state.page = 2 // URL: ?search=shoes&filter=active&page=2
session - Array of properties stored in sessionStorage
state.setup({
session: ['user', 'preferences', 'cart']
})
// These persist until the browser session ends
state.user = { name: 'Alice' }
state.cart = [{ id: 1, name: 'Shoes' }]
local - Array of properties stored in localStorage
state.setup({
local: ['theme', 'language', 'settings']
})
// These persist permanently on the device
state.theme = 'dark'
state.language = 'en'
memory - Array of properties kept only in memory
state.setup({
memory: ['temp_data', 'ui_state', 'removeId']
})
// These exist only while the page is loaded
state.temp_data = { processing: true }
state.ui_state = { modal_open: false }
state.removeId = 123 // ID for confirmation dialog
emit_only - Array of properties that only trigger events without storage
state.setup({
emit_only: ['deleted', 'saved', 'error']
})
// These fire events but don't persist anywhere
state.emit('deleted', userId) // Triggers listeners without storing
autolink - Enable automatic link handling for SPA navigation
state.setup({
route: '/app/:section/:id',
autolink: true
})
// Clicks on matching links automatically update state instead of page reload
Complete configuration example
state.setup({
route: '/shop/:category/:product',
query: ['search', 'color', 'size', 'page'],
session: ['user', 'cart'],
local: ['theme', 'currency'],
memory: ['loading', 'errors', 'removeId'],
emit_only: ['deleted', 'saved'],
autolink: true
})
state.on()
Listen to state changes with event handlers.
state.on(properties, callback)
Single property
state.on('search', (changes) => {
console.log('Search changed to:', changes.search)
})
Multiple properties
state.on('search filter page', (changes) => {
console.log('Changed properties:', changes)
// changes object contains only the properties that changed
})
Note: state.on()
replaces any previous listener with the same property names, so you don't need to call state.off()
to avoid duplicate listeners.
Callback parameter
The callback receives a changes
object containing only the properties that changed:
state.on('user cart', (changes) => {
if (changes.user) {
console.log('User changed:', changes.user)
}
if (changes.cart) {
console.log('Cart changed:', changes.cart)
updateCartDisplay(changes.cart)
}
})
Async handlers
Event handlers can be async:
state.on('search category', async (changes) => {
const results = await fetchProducts(changes.search, changes.category)
state.products = results
})
state.emit()
Trigger events for emit-only properties without storing the value:
state.setup({
emit_only: ['deleted', 'saved', 'error']
})
// Fire event without persistence
state.emit('deleted', userId)
// Listen to emit-only events
state.on('deleted', ({ deleted }) => {
console.log('User deleted:', deleted)
refreshUserList()
})
state.init()
Initialize state from the current URL. Call this after your UI is mounted:
mounted() {
state.init() // Populates state from current URL and fires initial events
}
This is essential for SPAs to handle direct navigation to URLs with state parameters.
Storage behavior
URL parameters (route and query)
URL parameters are always strings:
state.page = 2
console.log(state.page) // "2" (string)
console.log(location.search) // "?page=2"
state.active = true
console.log(state.active) // "true" (string)
console.log(location.search) // "?active=true"
// Objects become "[object Object]" - not useful
state.filter = { color: 'red' }
console.log(state.filter) // "[object Object]" (string)
Browser storage (session and local)
Session and local storage use JSON serialization:
state.setup({ session: ['user'], local: ['settings'] })
// Objects are JSON.stringify'd on save, JSON.parse'd on read
state.user = { name: 'Alice', id: 123 }
console.log(typeof state.user.id) // 'number' (restored from JSON)
// Arrays work the same way
state.settings = ['dark-mode', 'notifications']
console.log(Array.isArray(state.settings)) // true (restored from JSON)
Memory storage
Memory storage keeps exact references to any JavaScript value:
state.setup({ memory: ['cache', 'handler', 'domRef'] })
// Any object type works
const data = new Map()
state.cache = data
console.log(state.cache === data) // true (same reference)
// Functions, DOM elements, etc.
state.handler = () => console.log('click')
state.domRef = document.querySelector('#myButton')
Memory use cases
Memory storage is perfect for temporary UI state:
state.setup({
memory: ['removeId', 'selectedItems', 'dragState']
})
// Confirmation dialog
function showDeleteConfirm(userId) {
state.removeId = userId // Store ID for confirmation
}
// In delete confirmation component
state.on('removeId', ({ removeId }) => {
if (removeId) {
this.querySelector('dialog').showModal()
}
})
Route patterns
Simple parameters
state.setup({
route: '/users/:id'
})
state.id = '123' // URL: /users/123
Multiple parameters
state.setup({
route: '/shop/:category/:product/:variant'
})
state.category = 'shoes'
state.product = 'sneakers'
state.variant = 'red-large'
// URL: /shop/shoes/sneakers/red-large
Optional parameters
Use query parameters for optional values:
state.setup({
route: '/products/:category',
query: ['color', 'size', 'page']
})
state.category = 'shoes' // URL: /products/shoes
state.color = 'red' // URL: /products/shoes?color=red
state.size = 'large' // URL: /products/shoes?color=red&size=large
Integration patterns
SPA root component
<!doctype dhtml>
<script>
import { hasSession, logout } from 'app'
import { state } from 'state'
state.setup({
query: ['type', 'query', 'start'],
emit_only: ['deleted'],
memory: ['removeId'],
route: '/app/:id',
autolink: true,
})
if (!hasSession()) location.href = '/login/'
</script>
<body>
<header>
<nav>
<a href="/"><img src="/img/logo.png" width="60" height="22"></a>
<a href="/app/">Contacts</a>
</nav>
<nav>
<button class="plain" :onclick="logout(); location.href = '/'">Logout</button>
</nav>
</header>
<main>
<article/>
</main>
<confirm-delete/>
<script>
state.on('id', ({ id }) => {
const wrap = this.root.querySelector('article')
this.mount(id ? 'contact-details' : 'contact-list', wrap)
})
mounted() {
console.log('app mounted')
state.init() // Initialize from current URL
}
</script>
</body>
Component with state
<product-filter>
<input type="search"
value="{ state.search }"
:oninput="handleSearch">
<select :onchange="handleCategory">
<option value="">All categories</option>
<option :each="cat in categories" value="{ cat }">{ cat }</option>
</select>
<div :each="product in products" key="{ product.id }">
<h3>{ product.name }</h3>
<p>${ product.price }</p>
</div>
<script>
import { state } from 'state'
state.setup({
query: ['search', 'category'],
memory: ['products', 'categories']
})
handleSearch(e) {
state.search = e.target.value
}
handleCategory(e) {
state.category = e.target.value
}
// React to state changes
state.on('search category', async () => {
const products = await fetchProducts(state.search, state.category)
state.products = products
this.update() // Trigger component re-render
})
async mounted() {
const categories = await fetchCategories()
state.categories = categories
// Initial load if there's existing state
if (state.search || state.category) {
const products = await fetchProducts(state.search, state.category)
state.products = products
}
this.update()
}
get products() {
return state.products || []
}
get categories() {
return state.categories || []
}
</script>
</product-filter>
Manual updates
Components need manual updates after async state changes:
state.on('user', async (changes) => {
const profile = await fetchUserProfile(changes.user.id)
state.profile = profile
this.update() // Required for component re-render
})
Initialization
Handle initial state from URLs:
// URL: /products/shoes?color=red&page=2
console.log(state.category) // 'shoes'
console.log(state.color) // 'red'
console.log(state.page) // 2
// State is automatically populated from the current URL
Error handling
Invalid route parameters
Route parameters that don't match the pattern are ignored:
state.setup({
route: '/users/:id'
})
// Current URL: /about
state.id = '123' // No effect, URL stays /about
Storage limitations
Browser storage has size limits. Large objects may not persist:
state.setup({ local: ['large_data'] })
try {
state.large_data = hugeMegabyteObject
} catch (error) {
console.log('Storage quota exceeded')
}
Type conversion errors
Invalid JSON in URL parameters falls back to string:
// URL: ?data=invalid-json
console.log(state.data) // 'invalid-json' (string)
console.log(typeof state.data) // 'string'