Reactive State Management

Package: @coherent.js/state Since: v1.0.0-beta.2

Overview

The @coherent.js/state package provides a comprehensive reactive state management solution for Coherent.js applications. It includes reactive observables, computed properties, state persistence, validation, and SSR-compatible state management.

Installation

npm install @coherent.js/state@beta
# or
pnpm add @coherent.js/state@beta
# or
yarn add @coherent.js/state@beta

Table of Contents

  1. Reactive State (Client-Side)
  2. SSR-Compatible State
  3. State Persistence
  4. State Validation
  5. Context API

Reactive State (Client-Side)

Observables

Create reactive values that automatically track changes:

import { observable, computed } from '@coherent.js/state';

// Create observable
const count = observable(0);
const doubled = computed(() => count.value * 2);

// Watch for changes
count.watch((newValue, oldValue) => {
  console.log(`Count changed from ${oldValue} to ${newValue}`);
});

// Update value
count.value = 5; // Triggers watcher and updates computed
console.log(doubled.value); // 10

Reactive State Class

For more complex state management:

import { createReactiveState } from '@coherent.js/state';

const appState = createReactiveState({
  user: { name: 'John', age: 30 },
  settings: { theme: 'dark' }
});

// Watch specific paths
appState.watch('user.name', (newName, oldName) => {
  console.log(`User name changed to ${newName}`);
});

// Update state
appState.set('user.name', 'Jane');
appState.set('settings.theme', 'light');

// Get values
console.log(appState.get('user.name')); // 'Jane'

Computed Properties

Create derived values that automatically update:

import { observable, computed } from '@coherent.js/state';

const firstName = observable('John');
const lastName = observable('Doe');

const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`;
});

console.log(fullName.value); // 'John Doe'

firstName.value = 'Jane';
console.log(fullName.value); // 'Jane Doe' (automatically updated)

SSR-Compatible State

Request-Scoped State

For server-side rendering, use non-reactive state that's scoped to a request:

import { createState } from '@coherent.js/state';

// In your request handler
function handleRequest(req, res) {
  const requestState = createState({
    userId: req.user.id,
    requestId: req.id,
    startTime: Date.now()
  });

  // Use state during rendering
  requestState.set('theme', 'dark');
  const theme = requestState.get('theme');

  // State is automatically cleaned up after request
}

Global State Manager

Share state across the application during SSR:

import { globalStateManager } from '@coherent.js/state';

// Set global state
globalStateManager.set('appVersion', '1.0.0');
globalStateManager.set('config', { apiUrl: 'https://api.example.com' });

// Get global state
const version = globalStateManager.get('appVersion');
const config = globalStateManager.get('config');

// Clear when needed
globalStateManager.clear('appVersion');

Context API

Share state across components during SSR:

import { provideContext, useContext } from '@coherent.js/state';

// Provide context during rendering
function renderApp() {
  const requestState = { userId: 123, theme: 'dark' };

  provideContext('request', requestState);

  // Render your components
  return renderComponents();
}

// Use context in components
function UserProfile() {
  const requestState = useContext('request');
  const userId = requestState.userId;

  return {
    div: {
      text: `User ID: ${userId}`
    }
  };
}

State Persistence

LocalStorage Persistence

Automatically persist state to localStorage:

import { withLocalStorage } from '@coherent.js/state';

const userPrefs = withLocalStorage(
  { theme: 'dark', lang: 'en' },
  'user-prefs'
);

// State is automatically loaded from localStorage
console.log(userPrefs.get('theme')); // 'dark' (loaded from storage)

// Updates are automatically persisted
userPrefs.set('theme', 'light'); // Saved to localStorage

SessionStorage Persistence

For session-scoped state:

import { withSessionStorage } from '@coherent.js/state';

const sessionData = withSessionStorage(
  { cart: [], checkoutStep: 1 },
  'session-data'
);

sessionData.set('cart', [{ id: 1, name: 'Product' }]);
// Persisted to sessionStorage

IndexedDB Persistence

For larger datasets:

import { withIndexedDB } from '@coherent.js/state';

const largeDataset = await withIndexedDB(
  { data: [] },
  'app-data',
  { dbName: 'myApp', storeName: 'state' }
);

// Works asynchronously with IndexedDB
await largeDataset.set('data', hugeArray);

Custom Persistence

Create your own persistence strategy:

import { createPersistentState } from '@coherent.js/state';

const customState = createPersistentState(
  { count: 0 },
  {
    save: async (state) => {
      // Save to your backend
      await fetch('/api/state', {
        method: 'POST',
        body: JSON.stringify(state)
      });
    },
    load: async () => {
      // Load from your backend
      const response = await fetch('/api/state');
      return response.json();
    }
  }
);

State Validation

Built-in Validators

Validate state changes automatically:

import { createValidatedState, validators } from '@coherent.js/state';

const userForm = createValidatedState(
  { email: '', age: 0, username: '' },
  {
    validators: {
      email: validators.email('Invalid email format'),
      age: validators.range(18, 120, 'Age must be between 18 and 120'),
      username: validators.minLength(3, 'Username must be at least 3 characters')
    }
  }
);

// Valid update
userForm.set('email', 'user@example.com'); // ✓

// Invalid update
try {
  userForm.set('email', 'invalid-email'); // ✗ Throws validation error
} catch (error) {
  console.error(error.message); // 'Invalid email format'
}

Available Validators

import { validators } from '@coherent.js/state';

// String validators
validators.required('Field is required');
validators.email('Invalid email');
validators.minLength(5, 'Too short');
validators.maxLength(100, 'Too long');
validators.pattern(/^[a-z]+$/, 'Only lowercase letters');

// Number validators
validators.range(0, 100, 'Must be 0-100');
validators.min(0, 'Must be positive');
validators.max(100, 'Too large');

// Custom validators
validators.custom((value) => {
  if (value !== 'expected') {
    return 'Value must be "expected"';
  }
  return null; // null means valid
});

// Async validators
validators.async(async (value) => {
  const exists = await checkUsernameExists(value);
  return exists ? 'Username already taken' : null;
});

Custom Validation

const state = createValidatedState(
  { password: '', confirmPassword: '' },
  {
    validators: {
      password: (value) => {
        if (value.length < 8) return 'Password too short';
        if (!/[A-Z]/.test(value)) return 'Must contain uppercase';
        if (!/[0-9]/.test(value)) return 'Must contain number';
        return null;
      }
    },
    // Validate entire state object
    validate: (state) => {
      if (state.password !== state.confirmPassword) {
        throw new Error('Passwords do not match');
      }
    }
  }
);

Complete Example

Combining all features:

import {
  createReactiveState,
  withLocalStorage,
  createValidatedState,
  validators,
  provideContext
} from '@coherent.js/state';

// 1. Create validated, persistent reactive state
const appState = createValidatedState(
  { user: null, theme: 'dark', notifications: [] },
  {
    validators: {
      theme: validators.custom((value) => {
        return ['dark', 'light'].includes(value) ? null : 'Invalid theme';
      })
    }
  }
);

// 2. Persist to localStorage
const persistentState = withLocalStorage(appState, 'app-state');

// 3. Provide via context for SSR
provideContext('appState', persistentState);

// 4. Watch for changes
persistentState.watch('theme', (newTheme) => {
  document.body.setAttribute('data-theme', newTheme);
});

// 5. Update state
persistentState.set('theme', 'light'); // Validated, persisted, and reactive

API Reference

Observable

class Observable<T> {
  value: T;
  watch(callback: (newValue: T, oldValue: T) => void): () => void;
  unwatch(callback: Function): void;
  unwatchAll(): void;
}

Computed

class Computed<T> {
  readonly value: T;
  watch(callback: (newValue: T, oldValue: T) => void): () => void;
}

State Manager

interface StateManager {
  get(key: string): any;
  set(key: string, value: any): this;
  has(key: string): boolean;
  delete(key: string): boolean;
  clear(): this;
  toObject(): Record<string, any>;
}

Comparison with Core State

The @coherent.js/core package includes withState for component-level state management during SSR. Use @coherent.js/state when you need:

  • Reactive state: Automatic UI updates on state changes (client-side)
  • Persistence: LocalStorage, SessionStorage, or IndexedDB
  • Validation: Built-in validators for data integrity
  • Global state: Shared state across multiple components
  • Advanced features: Computed properties, watchers, async state

Use withState from @coherent.js/core for:

  • Simple component state during SSR
  • Request-scoped state
  • No need for reactivity or persistence

See Also