Advanced Features
Learn about YakaJS's Vuex-style stores, signals, and reactive programming
YakaJS provides powerful state management features inspired by Vuex, Redux, and SolidJS - all built-in!
YakaJS offers two approaches to state management:
Create a centralized store for your application state:
const store = _.createStore({ state: { count: 0, user: null, todos: [] }, mutations: { increment(state) { state.count++; }, decrement(state) { state.count--; }, setUser(state, user) { state.user = user; }, addTodo(state, todo) { state.todos.push(todo); }, removeTodo(state, id) { state.todos = state.todos.filter(t => t.id !== id); } }, actions: { async fetchUser({ commit }) { const user = await fetch('/api/user').then(r => r.json()); commit('setUser', user); }, async incrementAsync({ commit }) { setTimeout(() => { commit('increment'); }, 1000); } }, getters: { doubleCount(state) { return state.count * 2; }, completedTodos(state) { return state.todos.filter(t => t.completed); }, todoCount(state) { return state.todos.length; } }});// Read state directlyconsole.log(store.state.count); // 0console.log(store.state.user); // nullMutations are synchronous changes to state:
// Commit a mutationstore.commit('increment');console.log(store.state.count); // 1// Commit with payloadstore.commit('setUser', { id: 1, name: 'John Doe'});// Commit multiple timesstore.commit('increment');store.commit('increment');console.log(store.state.count); // 3Actions can be asynchronous:
// Dispatch an actionstore.dispatch('fetchUser');// Dispatch with payloadstore.dispatch('addTodo', { id: Date.now(), text: 'Learn YakaJS', completed: false});// Actions can return promisesstore.dispatch('incrementAsync').then(() => { console.log('Increment complete!');});Getters are computed properties based on state:
console.log(store.getters.doubleCount); // Computed valueconsole.log(store.getters.completedTodos); // Filtered arrayconsole.log(store.getters.todoCount); // CountYakaJS stores support undo/redo out of the box!
// Make some changesstore.commit('increment'); // count: 1store.commit('increment'); // count: 2store.commit('increment'); // count: 3// Undostore.undo(); // count: 2store.undo(); // count: 1// Redostore.redo(); // count: 2store.redo(); // count: 3// Check if undo/redo availableif (store.canUndo()) { store.undo();}if (store.canRedo()) { store.redo();}Watch for state changes:
// Subscribe to all mutationsstore.subscribe((mutation, state) => { console.log('Mutation:', mutation.type); console.log('Payload:', mutation.payload); console.log('New state:', state);});// Subscribe to specific mutationsstore.subscribe((mutation, state) => { if (mutation.type === 'increment') { console.log('Count incremented:', state.count); }});Signals are reactive values that automatically track dependencies:
// Create a signalconst count = _.signal(0);// Read value (call as function)console.log(count()); // 0// Set valuecount.set(5);console.log(count()); // 5// Update value based on currentcount.update(n => n + 1);console.log(count()); // 6Computed values automatically update when dependencies change:
const count = _.signal(10);const doubled = _.computed(() => count() * 2);console.log(doubled()); // 20count.set(5);console.log(doubled()); // 10count.update(n => n + 3);console.log(doubled()); // 16Effects run automatically when their dependencies change:
const name = _.signal('John');const age = _.signal(25);// Create effect that runs when dependencies change_.effect(() => { console.log(`${name()} is ${age()} years old`);});// Logs: "John is 25 years old"name.set('Jane');// Logs: "Jane is 25 years old"age.set(30);// Logs: "Jane is 30 years old"Combine signals with DOM manipulation:
const count = _.signal(0);// Update DOM when count changes_.effect(() => { _('#counter').text(count());});// Button clicks update signal_('#increment').on('click', () => { count.update(n => n + 1);});_('#decrement').on('click', () => { count.update(n => n - 1);});const todoStore = _.createStore({ state: { todos: [], filter: 'all' // 'all', 'active', 'completed' }, mutations: { addTodo(state, text) { state.todos.push({ id: Date.now(), text, completed: false }); }, toggleTodo(state, id) { const todo = state.todos.find(t => t.id === id); if (todo) todo.completed = !todo.completed; }, removeTodo(state, id) { state.todos = state.todos.filter(t => t.id !== id); }, setFilter(state, filter) { state.filter = filter; } }, getters: { filteredTodos(state) { if (state.filter === 'active') { return state.todos.filter(t => !t.completed); } if (state.filter === 'completed') { return state.todos.filter(t => t.completed); } return state.todos; }, activeCount(state) { return state.todos.filter(t => !t.completed).length; } }});// Add todo_('#addTodo').on('click', () => { const text = _('#todoInput').val(); if (text) { todoStore.commit('addTodo', text); _('#todoInput').val(''); renderTodos(); }});// Render todosfunction renderTodos() { const todos = todoStore.getters.filteredTodos; const html = todos.map(todo => ` <li data-id="${todo.id}" class="${todo.completed ? 'completed' : ''}"> <input type="checkbox" ${todo.completed ? 'checked' : ''}> <span>${todo.text}</span> <button class="delete">Delete</button> </li> `).join(''); _('#todoList').html(html);}// Toggle todo_('#todoList').on('click', 'input[type="checkbox"]', function() { const id = _(this).parent().attr('data-id'); todoStore.commit('toggleTodo', Number(id)); renderTodos();});// Delete todo_('#todoList').on('click', '.delete', function() { const id = _(this).parent().attr('data-id'); todoStore.commit('removeTodo', Number(id)); renderTodos();});// Create reactive counterconst count = _.signal(0);const message = _.computed(() => { const n = count(); if (n === 0) return 'Start counting!'; if (n < 10) return `Count: ${n}`; if (n < 20) return `Getting high: ${n}`; return `Wow! ${n}`;});// Auto-update DOM_.effect(() => { _('#count').text(count()); _('#message').text(message());});// Button actions_('#increment').on('click', () => count.update(n => n + 1));_('#decrement').on('click', () => count.update(n => n - 1));_('#reset').on('click', () => count.set(0));const cartStore = _.createStore({ state: { items: [], discount: 0 }, mutations: { addItem(state, product) { const existing = state.items.find(i => i.id === product.id); if (existing) { existing.quantity++; } else { state.items.push({ ...product, quantity: 1 }); } }, removeItem(state, productId) { state.items = state.items.filter(i => i.id !== productId); }, updateQuantity(state, { productId, quantity }) { const item = state.items.find(i => i.id === productId); if (item) { item.quantity = Math.max(0, quantity); } }, setDiscount(state, discount) { state.discount = discount; }, clearCart(state) { state.items = []; } }, getters: { subtotal(state) { return state.items.reduce((sum, item) => { return sum + (item.price * item.quantity); }, 0); }, total(state, getters) { const subtotal = getters.subtotal; return subtotal - (subtotal * state.discount / 100); }, itemCount(state) { return state.items.reduce((sum, item) => sum + item.quantity, 0); } }});// Subscribe to cart changescartStore.subscribe((mutation, state) => { // Update cart icon badge const count = cartStore.getters.itemCount; _('#cartBadge').text(count).toggle(count > 0); // Update total _('#cartTotal').text(`$${cartStore.getters.total.toFixed(2)}`);});// Add to cart_('.add-to-cart').on('click', function() { const product = { id: _(this).attr('data-id'), name: _(this).attr('data-name'), price: parseFloat(_(this).attr('data-price')) }; cartStore.commit('addItem', product);});const authStore = _.createStore({ state: { user: null, token: localStorage.getItem('token'), loading: false }, mutations: { setUser(state, user) { state.user = user; }, setToken(state, token) { state.token = token; if (token) { localStorage.setItem('token', token); } else { localStorage.removeItem('token'); } }, setLoading(state, loading) { state.loading = loading; } }, actions: { async login({ commit }, { email, password }) { commit('setLoading', true); try { const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }) }); const data = await response.json(); if (data.token) { commit('setToken', data.token); commit('setUser', data.user); return { success: true }; } return { success: false, error: data.error }; } finally { commit('setLoading', false); } }, async logout({ commit }) { commit('setToken', null); commit('setUser', null); }, async checkAuth({ commit, state }) { if (!state.token) return; try { const response = await fetch('/api/me', { headers: { 'Authorization': `Bearer ${state.token}` } }); if (response.ok) { const user = await response.json(); commit('setUser', user); } else { commit('setToken', null); } } catch (error) { console.error('Auth check failed:', error); } } }, getters: { isAuthenticated(state) { return !!state.user; }, userName(state) { return state.user?.name || 'Guest'; } }});// Check auth on loadauthStore.dispatch('checkAuth');// Subscribe to auth changesauthStore.subscribe((mutation, state) => { if (mutation.type === 'setUser') { // Update UI if (state.user) { _('#userName').text(state.user.name); _('#loginBtn').hide(); _('#logoutBtn').show(); } else { _('#userName').text('Guest'); _('#loginBtn').show(); _('#logoutBtn').hide(); } }});Use Store for:
Use Signals for:
You can use stores and signals together:
// Global store for app stateconst appStore = _.createStore({ /* ... */ });// Local signals for component statefunction createCounter() { const count = _.signal(0); const doubled = _.computed(() => count() * 2); return { count, doubled };}Split large stores into modules:
// User moduleconst userModule = { state: { user: null }, mutations: { setUser(state, user) { state.user = user; } }};// Cart moduleconst cartModule = { state: { items: [] }, mutations: { addItem(state, item) { state.items.push(item); } }};// Combine modulesconst store = _.createStore({ state: { ...userModule.state, ...cartModule.state }, mutations: { ...userModule.mutations, ...cartModule.mutations }});Batch multiple mutations:
// Instead of multiple commitsstore.commit('setName', 'John');store.commit('setAge', 30);store.commit('setEmail', 'john@example.com');// Use a single mutationstore.commit('setUser', { name: 'John', age: 30, email: 'john@example.com'});Complex getters are automatically memoized:
getters: { expensiveComputation(state) { // This runs only when state.data changes return state.data.map(/* expensive operation */); }}// Good: Computed depends on other reactive valuesconst doubled = _.computed(() => count() * 2);// Avoid: Expensive operations in computedconst bad = _.computed(() => { // Avoid API calls or heavy processing return heavyProcessing();});State management in YakaJS is powerful yet simple. Choose the approach that fits your needs!
For more details, see the API Reference.