Quick Start
Nounjs provides a complete state management solution with global stores, watchers, computed properties, and actions.
import { createStore, Component, mount } from '@noundryfx/nounjs'
// Create a global store
const store = createStore({
state: {
user: null,
cart: [],
theme: 'light'
}
})
// Component A - Updates the store
class UserPanel extends Component {
template = `<button @click="login">Login</button>`
login() {
store.set('user', { name: 'John', email: 'john@example.com' })
}
}
// Component B - Reacts to store changes
class Header extends Component {
user = null
template = `
<header>
<span :if="user">Welcome, { user.name }!</span>
<span :else>Please login</span>
</header>
`
mounted() {
// Watch store and sync to local state (auto-updates DOM)
this.$watchStore(store, 'user', (newUser) => {
this.user = newUser
})
}
}
Creating a Store
Basic Store
import { createStore } from '@noundryfx/nounjs'
const store = createStore({
state: {
count: 0,
todos: [],
user: null
}
})
Named Stores
// Create multiple stores
const userStore = createStore('user', {
state: { profile: null, preferences: {} }
})
const cartStore = createStore('cart', {
state: { items: [], total: 0 }
})
// Access later by name
import { getStore } from '@noundryfx/nounjs'
const cart = getStore('cart')
Reading & Writing State
// Get single value
const user = store.get('user')
// Get nested value (dot notation)
const city = store.get('user.address.city')
// Get entire state
const state = store.get() // or store.getState()
// Set single value
store.set('count', 5)
// Set nested value (creates path if needed)
store.set('user.address.city', 'New York')
// Update multiple values
store.update({
'user.name': 'John',
'user.email': 'john@example.com',
'settings.theme': 'dark'
})
// Batch updates (single notification)
store.batch(() => {
store.set('loading', true)
store.set('error', null)
store.set('data', newData)
})
// Watchers notified once at the end
Watchers
React to state changes in real-time.
// Watch specific path
const unwatch = store.watch('user', (newValue, oldValue, path) => {
console.log('User changed:', newValue)
})
// Watch nested path
store.watch('user.preferences.theme', (theme) => {
document.body.className = theme
})
// Watch all changes
store.watch('*', (newValue, oldValue, path) => {
console.log(`${path} changed to:`, newValue)
})
// Stop watching
unwatch()
Deep Reactivity
Watching a parent path fires on any nested change:
store.watch('user', (user) => console.log('Changed'))
// All of these trigger the watcher:
store.set('user.name', 'Jane')
store.set('user.address.city', 'LA')
store.set('user.address.zip', '90001')
Computed Properties
Cached derived values that update when dependencies change.
const store = createStore({
state: {
items: [],
taxRate: 0.1
},
computed: {
// Cached - only recalculates when dependencies change
subtotal() {
return this.get('items').reduce((sum, item) => sum + item.price, 0)
},
tax() {
return this.get('subtotal') * this.get('taxRate')
},
total() {
return this.get('subtotal') + this.get('tax')
},
itemCount() {
return this.get('items').length
}
}
})
// Access computed properties like regular state
store.get('total') // Calculates and caches
store.get('itemCount') // Returns cached value
Actions
Organized state mutations, including async operations.
const store = createStore({
state: {
user: null,
loading: false,
error: null
},
actions: {
async login({ set, batch }, credentials) {
set('loading', true)
set('error', null)
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
})
const user = await response.json()
batch(() => {
set('user', user)
set('loading', false)
})
} catch (err) {
batch(() => {
set('error', err.message)
set('loading', false)
})
}
},
logout({ set }) {
set('user', null)
},
addToCart({ get, set }, item) {
const items = [...get('cart.items'), item]
set('cart.items', items)
}
}
})
// Dispatch actions
store.dispatch('login', { email: 'john@example.com', password: '***' })
store.dispatch('logout')
store.dispatch('addToCart', { id: 1, name: 'Product', price: 29.99 })
Component Integration
$watchStore
Watch store from component (auto-cleaned on unmount):
class Dashboard extends Component {
stats = null
mounted() {
this.$watchStore(store, 'analytics.stats', (stats) => {
this.stats = stats // Auto-updates DOM
})
}
}
$connect
Map store paths to local properties:
class UserProfile extends Component {
user = null
theme = 'light'
mounted() {
// Map store paths to local properties
this.$connect(store, {
user: 'user',
theme: 'settings.theme'
})
}
}
$watch - Local State
class Counter extends Component {
count = 0
mounted() {
// Watch local state property
this.$watch('count', (newVal, oldVal) => {
console.log(`Count changed from ${oldVal} to ${newVal}`)
store.set('counter.value', newVal) // Sync to store
})
// Immediate: call with current value right away
this.$watch('count', (val) => {
console.log('Current count:', val)
}, { immediate: true })
}
increment() {
this.count++ // Triggers watcher and auto-updates DOM
}
}
Array Reactivity
Arrays remain reactive even after reassignment.
class TodoList extends Component {
items = ['a', 'b', 'c']
mounted() {
// Reassign array - still reactive
this.items = [...this.items, 'd']
// Mutations on new array work correctly
this.items.push('e') // Triggers update
this.items.pop() // Triggers update
this.items.splice(0,1) // Triggers update
}
}
// Store array reactivity
store.watch('todos', (todos) => {
console.log('Todos changed:', todos.length)
})
const todos = store.get('todos')
todos.push({ text: 'New todo' }) // Triggers watcher
todos.splice(0, 1) // Triggers watcher
todos[0] = { text: 'Updated' } // Triggers watcher
Custom remove() Method
Nounjs adds a remove() method to reactive arrays:
// Remove by item reference
this.items.remove(item)
// Equivalent to:
const idx = this.items.indexOf(item)
if (idx > -1) this.items.splice(idx, 1)
Real-World Example: Theme Store
The Nounjs Starter app demonstrates a complete theme management system using global stores and $watchStore.
Theme Switcher in Nounjs Starter
See src/lib/store.js and src/components/themeswitcher.noun for the complete implementation.
Theme Store Definition
// src/lib/store.js
export const themeStore = createStore('theme', {
state: () => ({
mode: 'light', // 'light' | 'dark'
primaryColor: 'blue'
}),
actions: {
init({ set, get }) {
const saved = storage.local.get('theme')
if (saved?.mode) set('mode', saved.mode)
this.apply({ get })
},
setMode({ set, get }, mode) {
set('mode', mode)
this.apply({ get })
this.persist({ get })
},
apply({ get }) {
if (get('mode') === 'dark') {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
},
persist({ get }) {
storage.local.set('theme', {
mode: get('mode'),
primaryColor: get('primaryColor')
})
}
}
})
themeStore.dispatch('init')
Component with $watchStore
// src/components/themeswitcher.noun
export default {
currentMode: 'light',
mounted() {
const store = this.props?.themeStore
if (store) {
// Get initial state
this.currentMode = store.get('mode')
// Watch for changes - auto-updates UI
this.$watchStore(store, 'mode', (newMode) => {
this.currentMode = newMode
})
}
},
setMode(mode) {
this.props?.themeStore?.dispatch('setMode', mode)
}
}
// Template uses currentMode reactively:
// <button @click="setMode('dark')">
// { currentMode === 'dark' ? 'Dark' : 'Light' }
// </button>
Key Patterns Demonstrated
- State factory:
state: () => ({})for fresh state per store instance - Actions: Destructured helpers
{ set, get, batch } - Persistence: Using
storage.localfor localStorage - Component binding:
$watchStore()for reactive UI updates - Loose coupling: Store passed via props for testability
Debugging State
Use the built-in DevTools to inspect and edit store state in real-time.
Native DevTools
Enable DevTools to see all component state, edit values live, and track state change events.
API Reference
Store Methods
| Method | Description |
|---|---|
get(path?) | Get state value at path |
set(path, value) | Set state value at path |
update(updates) | Update multiple values |
batch(fn) | Batch updates, single notification |
watch(path, callback) | Watch path for changes |
dispatch(action, payload) | Dispatch an action |
reset(newState) | Reset state |
Component Methods
| Method | Description |
|---|---|
$watch(prop, callback, options?) | Watch local state property |
$watchStore(store, path, callback) | Watch store path |
$connect(store, mappings) | Map store state to local state |
Factory Functions
| Function | Description |
|---|---|
createStore(options) | Create new store |
createStore(name, options) | Create named store |
getStore(name?) | Get store by name |
hasStore(name) | Check if store exists |
deleteStore(name) | Delete a store |
clearStores() | Clear all stores |