Fluxo UIFluxo UIv0.4.1

Basic Store

A lightweight state management solution with batched updates, computed properties, path-based subscriptions, and a composable middleware system.

Basic Usage

Counter Store

A simple counter using create() and createHook() for React integration

0
import { create, createHook } from 'fluxo-ui/store';

interface CounterState {
  count: number;
}

const counterStore = create<CounterState>(() => ({ count: 0 }));
const useCounter = createHook(counterStore);

function Counter() {
  const { count } = useCounter();

  return (
    <div>
      <span>Count: {count}</span>
      <Button label="Increment"
        onClick={() => counterStore.setState(s => ({ count: s.count + 1 }))} />
      <Button label="Decrement"
        onClick={() => counterStore.setState(s => ({ count: s.count - 1 }))} />
      <Button label="Reset" variant="secondary"
        onClick={() => counterStore.reset()} />
    </div>
  );
}

Computed Properties

Synchronous Computed Properties

Derived state that updates automatically when dependencies change

JD
John Doe
Computed from firstName + lastName
import { create, createHook } from 'fluxo-ui/store';

const userStore = create<UserState>(() => ({
  firstName: 'John',
  lastName: 'Doe',
}));

userStore.compute(
  'fullName',
  (state) => `${state.firstName} ${state.lastName}`,
  ['firstName', 'lastName']
);

userStore.compute(
  'initials',
  (state) => `${state.firstName.charAt(0)}${state.lastName.charAt(0)}`.toUpperCase(),
  ['firstName', 'lastName']
);

const useUser = createHook(userStore);

function UserCard() {
  const state = useUser();
  const { firstName, lastName, fullName, initials } = state as any;

  return (
    <div>
      <div className="avatar">{initials}</div>
      <div>{fullName}</div>
      <TextInput value={firstName}
        onChange={(e) => userStore.setState({ firstName: e.value })} />
      <TextInput value={lastName}
        onChange={(e) => userStore.setState({ lastName: e.value })} />
    </div>
  );
}

Async Computed Properties

Computed properties that resolve asynchronously with automatic loading state

Select a user to load their profile asynchronously
Loading profile...
import { create, createHook } from 'fluxo-ui/store';

const asyncStore = create<{ userId: number }>(() => ({ userId: 1 }));

asyncStore.compute(
  'profile',
  async (state) => {
    const res = await fetch(`/api/users/${state.userId}`);
    return res.json();
  },
  ['userId']
);

const useStore = createHook(asyncStore);

function UserProfile() {
  const state = useStore() as any;
  // state.profile       → the resolved value (undefined while loading)
  // state.profileLoading → boolean (true while fetching)

  if (state.profileLoading) return <p>Loading profile...</p>;
  return <p>{state.profile}</p>;
}

Batched Updates

Batched Updates

Multiple setState calls within one synchronous handler are batched into a single re-render

Count
0
Renders
1
Three setState calls in one handler produce only one re-render thanks to microtask batching. In React dev mode (StrictMode), render count may increment by 2 — this is expected and does not occur in production builds.
import { create, createHook } from 'fluxo-ui/store';

const store = create<{ count: number }>(() => ({ count: 0 }));
const useStore = createHook(store);

function BatchDemo() {
  const renderCount = useRef(0);
  renderCount.current++;

  const { count } = useStore();

  const handleBatchIncrement = () => {
    // All three calls are batched into a single re-render
    store.setState((s) => ({ count: s.count + 1 }));
    store.setState((s) => ({ count: s.count + 1 }));
    store.setState((s) => ({ count: s.count + 1 }));
  };

  return (
    <div>
      <p>Count: {count}</p>
      <p>Render count: {renderCount.current}</p>
      <Button label="+3 (Batched)" onClick={handleBatchIncrement} />
    </div>
  );
}

Path Subscriptions

Path-Based Subscriptions

Each component subscribes to a specific slice of state using useProfile(selector, true). The second argument enables shallow comparison. Change user data — only UserCard re-renders. Change settings — only SettingsPanel re-renders. The Full State Watcher (no selector) re-renders on every change.

User Card
Renders:1
useProfile((s) => s.user, true)
Name: Alice
Email: alice@example.com
Role: Admin
Settings Panel
Renders:1
useProfile((s) => s.settings, true)
Theme: light
Notifications:
OffOn
Language: English
Metadata Display
Renders:1
useProfile((s) => s.metadata, true)
Last Login: 4:25:09 PM
Version: v1
Full State Watcher (no selector)
Renders:1
useProfile() — re-renders on every change
{
  "user": {
    "name": "Alice",
    "email": "alice@example.com",
    "role": "Admin"
  },
  "settings": {
    "theme": "light",
    "notifications": true,
    "language": "English"
  },
  "metadata": {
    "lastLogin": "4:25:09 PM",
    "version": 1
  }
}
External Path Subscription Log
Click buttons above to see which path subscriptions fire
import { create, createHook } from 'fluxo-ui/store';

const profileStore = create<ProfileState>(() => ({
  user: { name: 'Alice', email: 'alice@example.com', role: 'Admin' },
  settings: { theme: 'light', notifications: true, language: 'English' },
  metadata: { lastLogin: '...', version: 1 },
}));

const useProfile = createHook(profileStore);

// Hook with selector + shallow equality (true = shallow compare)
// Only re-renders when the user object reference changes
function UserCard() {
  const user = useProfile((s) => s.user, true);
  return <div>{user.name} ({user.role})</div>;
}

// Hook with selector — only re-renders when settings change
function SettingsPanel() {
  const settings = useProfile((s) => s.settings, true);
  return <div>{settings.theme} / {settings.language}</div>;
}

// Hook with no selector — re-renders on EVERY state change
function FullStateWatcher() {
  const state = useProfile();
  return <pre>{JSON.stringify(state, null, 2)}</pre>;
}

// External (non-React) path subscription
profileStore.on('change', 'user.name', (state) => {
  console.log('Name changed:', state.user.name);
});

profileStore.on('change', 'settings', (state) => {
  console.log('Settings changed:', state.settings);
});

Multiple Stores

Multiple Independent Stores

Two separate stores (tasks + notifications) coexist in one component tree. Each store has its own hook — updating one never causes re-renders in components subscribed to the other. Watch the render counts to verify isolation.

Task Store
Renders: 1
1 of 3 completed33%
Set up project
Build components
Write tests
Notification Store
Renders: 1
Muted
OffOn
No notifications yet
Cross-store actions (updates both stores independently)
import { create, createHook } from 'fluxo-ui/store';

// Store A — Task management
const taskStore = create<TaskState>(() => ({
  tasks: [{ id: 1, title: 'Build components', done: false }],
  nextId: 2,
  filter: 'all',
}));
taskStore.compute('completedCount', (s) => s.tasks.filter(t => t.done).length, ['tasks']);
taskStore.compute('progress', (s) => {
  const done = s.tasks.filter(t => t.done).length;
  return s.tasks.length ? Math.round((done / s.tasks.length) * 100) : 0;
}, ['tasks']);
const useTaskStore = createHook(taskStore);

// Store B — Notification feed
const notificationStore = create<NotificationState>(() => ({
  items: [],
  nextId: 1,
  muted: false,
}));
const useNotificationStore = createHook(notificationStore);

// Each store is fully independent — updating one never triggers
// re-renders in components that only subscribe to the other.
function TaskPanel() {
  const { tasks, filter } = useTaskStore();
  // Only re-renders when taskStore changes
}

function NotificationFeed() {
  const { items, muted } = useNotificationStore();
  // Only re-renders when notificationStore changes
}

Import

import { create, createHook } from 'fluxo-ui/store';
import {
  persistMiddleware,
  undoRedoMiddleware,
  validationMiddleware,
  loggerMiddleware,
  throttleMiddleware,
  devToolsMiddleware,
} from 'fluxo-ui/store/middlewares';

API Reference

create(initializer, middlewares?)
(initializer: () => T, middlewares?: Middleware<T>[]) => Store<T>"-"

Create a new store with initial state and optional middleware

store.getState()
() => T"-"

Get the current state snapshot (includes computed properties)

store.setState(update)
(partial: Partial<T>) => void"-"

Merge partial state into current state (batched via microtask)

store.setState(updater)
(fn: (state: T) => Partial<T>) => void"-"

Update state using an updater function for safe reads

store.on(event, listener)
(event: 'init' | 'change', listener) => unsubscribe"-"

Subscribe to all state changes or initialization

store.on(event, path, listener)
(event: 'change', path: string, listener) => unsubscribe"-"

Subscribe to changes on a specific state path

store.reset()
() => void"-"

Reset state to the initial value from the initializer function

store.compute(name, fn, deps)
(name: string, fn: (state: T) => R, deps: string[]) => void"-"

Register a computed property with dependency tracking

createHook(store)
(store: Store<T>) => useStore"-"

Create a React hook bound to a store for reactive component integration

useStore(selector?, equalityFn?)
(selector?, equalityFn?) => T | R"-"

React hook returned by createHook. Optional selector for derived slices

Features

Microtask Batching

Multiple setState calls within one synchronous handler are batched into a single notification, minimizing re-renders

Computed Properties

Derive values from state with automatic dependency tracking and caching for optimal performance

Path-Based Subscriptions

Listen to specific state paths for fine-grained reactivity, avoiding unnecessary work when unrelated state changes

Middleware System

Extensible middleware for persistence, undo/redo, validation, throttling, logging, and devtools integration

Immutable Updates

State is cloned via structuredClone on every update, ensuring safe immutable operations without external libraries

Framework Agnostic Core

The core store is plain TypeScript with no React dependency. React integration is provided through createHook()