Now.js Framework Documentation

Now.js Framework Documentation

StateManager

EN 15 Dec 2025 01:15

StateManager

Overview

StateManager is the global state management system in Now.js Framework that uses a Vuex/Redux-like pattern. It supports modules, mutations, actions, getters, history tracking, and persistence.

When to use:

  • Need global state shared across multiple components
  • Want to track state changes (debugging)
  • Need time-travel debugging
  • Want to persist state to localStorage

Why use it:

  • ✅ Module-based state organization
  • ✅ Mutations for synchronous changes
  • ✅ Actions for async operations
  • ✅ Getters for computed state
  • ✅ Watch and Subscribe for reactivity
  • ✅ History tracking and time-travel
  • ✅ Automatic persistence
  • ✅ Middleware support

Basic Usage

Registering a Module

// Register a module
StateManager.registerModule('counter', {
  // Initial state
  state: {
    count: 0,
    step: 1
  },

  // Synchronous state changes
  mutations: {
    increment(state, payload) {
      state.count += payload || state.step;
    },
    decrement(state, payload) {
      state.count -= payload || state.step;
    },
    setStep(state, step) {
      state.step = step;
    }
  },

  // Async operations
  actions: {
    async incrementAsync(context, delay = 1000) {
      await new Promise(resolve => setTimeout(resolve, delay));
      context.commit('increment');
    },
    async fetchAndSet(context, url) {
      const response = await fetch(url);
      const data = await response.json();
      context.commit('increment', data.value);
    }
  },

  // Computed values
  getters: {
    doubleCount(state) {
      return state.count * 2;
    },
    isPositive(state) {
      return state.count > 0;
    }
  }
});

Using State

// Read state
const count = StateManager.get('counter.count');
console.log(count); // 0

// Commit mutation (synchronous)
StateManager.commit('counter/increment');
StateManager.commit('counter/increment', 5);

// Dispatch action (async)
await StateManager.dispatch('counter/incrementAsync', 500);

// Watch changes
const unwatch = StateManager.watch('counter.count', (newValue) => {
  console.log('Count changed:', newValue);
});

// Later: stop watching
unwatch();

Modules

Module Structure

StateManager.registerModule('moduleName', {
  // State: initial data
  state: {
    items: [],
    loading: false,
    error: null
  },

  // Mutations: sync state changes
  mutations: {
    setItems(state, items) {
      state.items = items;
    },
    setLoading(state, loading) {
      state.loading = loading;
    },
    setError(state, error) {
      state.error = error;
    }
  },

  // Actions: async logic
  actions: {
    async fetchItems(context) {
      context.commit('setLoading', true);
      try {
        const response = await fetch('/api/items');
        const items = await response.json();
        context.commit('setItems', items);
      } catch (error) {
        context.commit('setError', error.message);
      } finally {
        context.commit('setLoading', false);
      }
    }
  },

  // Getters: computed values
  getters: {
    itemCount(state) {
      return state.items.length;
    },
    activeItems(state) {
      return state.items.filter(item => item.active);
    }
  },

  // Watch: track changes
  watch: {
    'items': (newItems) => {
      console.log('Items updated:', newItems.length);
    }
  },

  // Init: called when module is registered
  init(context) {
    console.log('Module initialized');
    context.dispatch('fetchItems');
  }
});

State Function

When state is a function, it creates a new instance each time:

StateManager.registerModule('form', {
  // Use function to prevent shared state
  state: () => ({
    fields: {},
    errors: [],
    dirty: false
  })
});

Force Replace Module

// Replace existing module
StateManager.registerModule('counter', {
  state: { count: 100 }
}, { force: true });

Mutations

Mutations are synchronous functions that change state:

// Define mutations
mutations: {
  // Simple mutation
  increment(state) {
    state.count++;
  },

  // With payload
  setUser(state, user) {
    state.user = user;
  },

  // With multiple values
  addItem(state, { id, name, price }) {
    state.items.push({ id, name, price });
  }
}

// Call mutations
StateManager.commit('moduleName/mutationName');
StateManager.commit('moduleName/mutationName', payload);

❌ Don't Use Async in Mutations

mutations: {
  // ❌ Bad - don't use async in mutations
  async fetchData(state) {
    const data = await fetch('/api/data');
    state.data = data;
  }
}

// ✅ Use actions instead
actions: {
  async fetchData(context) {
    const response = await fetch('/api/data');
    const data = await response.json();
    context.commit('setData', data);
  }
}

Actions

Actions are async functions that can perform side effects:

actions: {
  // Simple action
  async loadUser(context, userId) {
    context.commit('setLoading', true);

    try {
      const response = await fetch(`/api/users/${userId}`);
      if (!response.ok) throw new Error('Failed to load user');

      const user = await response.json();
      context.commit('setUser', user);
      return user;

    } catch (error) {
      context.commit('setError', error.message);
      throw error;

    } finally {
      context.commit('setLoading', false);
    }
  },

  // Action that dispatches other actions
  async initializeApp(context) {
    await context.dispatch('loadUser', 'current');
    await context.dispatch('loadSettings');
  }
}

// Call actions
const user = await StateManager.dispatch('users/loadUser', 123);

Context Object

Action context includes:

actions: {
  example(context, payload) {
    // State for this module
    console.log(context.state);

    // Getters for this module
    console.log(context.getters.itemCount);

    // Commit mutation
    context.commit('mutationName', payload);

    // Dispatch action
    await context.dispatch('otherAction', payload);

    // Watch changes
    context.watch('path', handler);
  }
}

Getters

Getters are computed values derived from state:

getters: {
  // Simple getter
  itemCount(state) {
    return state.items.length;
  },

  // Getter using other getters
  doubleCount(state, getters) {
    return getters.itemCount * 2;
  },

  // Filter items
  activeItems(state) {
    return state.items.filter(item => item.active);
  },

  // Search function
  findById(state) {
    return (id) => state.items.find(item => item.id === id);
  }
}

// Use getters
const context = StateManager.getModuleContext('moduleName');
console.log(context.getters.itemCount);
console.log(context.getters.findById(123));

Watch and Subscribe

Watch

Track state path changes:

// Watch single path
const unwatch = StateManager.watch('counter.count', (newValue) => {
  console.log('Count is now:', newValue);
});

// Stop watching
unwatch();

Subscribe

Subscribe with additional options:

// Subscribe with options
const unsubscribe = StateManager.subscribe('user.profile', (value) => {
  console.log('Profile updated:', value);
}, {
  immediate: true,  // Call immediately with current value
  deep: false       // Detect deep changes
});

// Stop subscribing
unsubscribe();

Watch in Module Definition

StateManager.registerModule('cart', {
  state: {
    items: [],
    total: 0
  },
  watch: {
    'items': function(newItems, oldItems) {
      // Recalculate total
      this.commit('updateTotal');
    }
  }
});

Time Travel

StateManager records state change history:

// View history
console.log(StateManager.history);
// [
//   { type: 'counter/increment', payload: null, state: {...}, timestamp: 1699...},
//   { type: 'counter/increment', payload: 5, state: {...}, timestamp: 1699...}
// ]

// Go back to previous state
StateManager.timeTravel(0); // Go to first entry

// View current index
console.log(StateManager.historyIndex); // 0

// Go forward
StateManager.timeTravel(StateManager.historyIndex + 1);

Configuration

await StateManager.init({
  // Debug mode - show logs
  debug: false,

  // Persistence settings
  persistence: {
    enabled: true,
    key: 'app_state',        // localStorage key
    blacklist: ['temp', 'ui'], // modules not to persist
    encrypt: false            // encrypt data
  },

  // History settings
  history: {
    enabled: true,
    maxSize: 50,              // max entries
    include: ['user', 'data'] // modules to track
  },

  // Batch updates
  batch: {
    enabled: true,
    delay: 16                 // ms (1 frame)
  },

  // Strict mode - log mutations
  strict: true
});

API Reference

StateManager.init(options)

Initialize StateManager

Parameter Type Description
options object Configuration options

Returns: Promise<StateManager>

StateManager.registerModule(name, module, options)

Register a module

Parameter Type Description
name string Module name
module object Module definition
options.force boolean Replace existing module

StateManager.hasModule(name)

Check if module exists

Parameter Type Description
name string Module name

Returns: boolean

StateManager.get(path)

Get state value

Parameter Type Description
path string Path like 'module.property'

Returns: any

const count = StateManager.get('counter.count');
const user = StateManager.get('auth.user');

StateManager.set(path, value)

Set state value directly (use sparingly)

Parameter Type Description
path string Path
value any New value

StateManager.commit(type, payload)

Call a mutation

Parameter Type Description
type string 'moduleName/mutationName'
payload any Data to pass to mutation
StateManager.commit('counter/increment');
StateManager.commit('counter/setCount', 100);

StateManager.dispatch(type, payload)

Call an action

Parameter Type Description
type string 'moduleName/actionName'
payload any Data to pass to action

Returns: Promise<any>

await StateManager.dispatch('auth/login', { email, password });
const users = await StateManager.dispatch('users/fetchAll');

StateManager.watch(path, callback)

Watch for changes

Parameter Type Description
path string State path
callback function Handler function

Returns: function - Unwatch function

StateManager.subscribe(path, callback, options)

Subscribe with options

Parameter Type Description
path string State path
callback function Handler function
options.immediate boolean Call immediately
options.deep boolean Deep comparison

Returns: function - Unsubscribe function

StateManager.timeTravel(index)

Go to state in history

Parameter Type Description
index number History index

StateManager.reset()

Reset all state to initial values

StateManager.persistState()

Save state to localStorage

StateManager.use(middleware)

Add middleware

Parameter Type Description
middleware object Middleware object

Real-World Examples

Authentication Module

StateManager.registerModule('auth', {
  state: () => ({
    user: null,
    token: null,
    loading: false,
    error: null
  }),

  mutations: {
    setUser(state, user) {
      state.user = user;
    },
    setToken(state, token) {
      state.token = token;
    },
    setLoading(state, loading) {
      state.loading = loading;
    },
    setError(state, error) {
      state.error = error;
    },
    logout(state) {
      state.user = null;
      state.token = null;
    }
  },

  actions: {
    async login(context, { email, password }) {
      context.commit('setLoading', true);
      context.commit('setError', null);

      try {
        const response = await fetch('/api/auth/login', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ email, password })
        });

        if (!response.ok) {
          throw new Error('Login failed');
        }

        const { user, token } = await response.json();
        context.commit('setUser', user);
        context.commit('setToken', token);

        return user;

      } catch (error) {
        context.commit('setError', error.message);
        throw error;

      } finally {
        context.commit('setLoading', false);
      }
    },

    async logout(context) {
      await fetch('/api/auth/logout', { method: 'POST' });
      context.commit('logout');
    }
  },

  getters: {
    isLoggedIn(state) {
      return !!state.user && !!state.token;
    },
    userName(state) {
      return state.user?.name || 'Guest';
    }
  }
});

// Usage
await StateManager.dispatch('auth/login', {
  email: 'user@example.com',
  password: 'password123'
});

const isLoggedIn = StateManager.getModuleContext('auth').getters.isLoggedIn;

Shopping Cart Module

StateManager.registerModule('cart', {
  state: () => ({
    items: [],
    coupon: null
  }),

  mutations: {
    addItem(state, product) {
      const existing = state.items.find(item => item.id === product.id);
      if (existing) {
        existing.quantity++;
      } else {
        state.items.push({ ...product, quantity: 1 });
      }
    },
    removeItem(state, productId) {
      state.items = state.items.filter(item => item.id !== productId);
    },
    updateQuantity(state, { productId, quantity }) {
      const item = state.items.find(item => item.id === productId);
      if (item) {
        item.quantity = Math.max(0, quantity);
        if (item.quantity === 0) {
          state.items = state.items.filter(item => item.id !== productId);
        }
      }
    },
    setCoupon(state, coupon) {
      state.coupon = coupon;
    },
    clearCart(state) {
      state.items = [];
      state.coupon = null;
    }
  },

  getters: {
    itemCount(state) {
      return state.items.reduce((sum, item) => sum + item.quantity, 0);
    },
    subtotal(state) {
      return state.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
    },
    discount(state, getters) {
      if (!state.coupon) return 0;
      return getters.subtotal * (state.coupon.discount / 100);
    },
    total(state, getters) {
      return getters.subtotal - getters.discount;
    }
  }
});

Common Pitfalls

⚠️ 1. Don't Modify State Directly

// ❌ Bad - modifying state directly
StateManager.state.counter.count++;

// ✅ Good - use mutation
StateManager.commit('counter/increment');

⚠️ 2. Watch Must Cleanup

// ❌ Memory leak
function setupComponent() {
  StateManager.watch('counter.count', updateUI);
}

// ✅ Cleanup when component is removed
function setupComponent() {
  const unwatch = StateManager.watch('counter.count', updateUI);

  return () => unwatch(); // Cleanup function
}

⚠️ 3. Actions Must Handle Errors

// ❌ Not handling errors
actions: {
  async fetchData(context) {
    const data = await fetch('/api/data').then(r => r.json());
    context.commit('setData', data);
  }
}

// ✅ Handle errors
actions: {
  async fetchData(context) {
    try {
      const response = await fetch('/api/data');
      if (!response.ok) throw new Error('Fetch failed');
      const data = await response.json();
      context.commit('setData', data);
    } catch (error) {
      context.commit('setError', error.message);
      throw error; // Re-throw so caller can handle
    }
  }
}