Fluxo UIFluxo UIv0.4.1

Model Store

A domain-model factory built on top of the base store. createModel provides entity-level CRUD, list pagination, validation, persistence, and React hooks for building data-driven UIs.

Basic CRUD

Basic CRUD Operations

Create, read, update, and delete todos using createModel

No todos yet. Add one above.

import { createModel, createItemHook } from 'fluxo-ui/store';

interface Todo {
    id: number;
    title: string;
    completed: boolean;
}

let nextTodoId = 1;

const todoFactory = createModel<Todo>({
    nextId: () => nextTodoId++,
    createWithDefaults: (id) => ({
        id, title: '', completed: false,
    }),
    selectId: (state) => state.id,
});

// Create a new todo
const store = todoFactory.create({ title: 'My Task' });

// Read state
const state = store.getState(); // { id: 1, title: 'My Task', completed: false }

// Update state
store.setState({ completed: true });

// Delete / destroy
store.destroy();

// React hook for a single item
const useItem = createItemHook(store);
function TodoItem() {
    const todo = useItem();
    return <span>{todo.title}</span>;
}

List Management

List Management with Pagination

Use list() to paginate and sort model items

Sort by:
NameCategoryPrice
No products. Add some above.
0 total items
1 / 1
import { createModel, createListHook } from 'fluxo-ui/store';

const productFactory = createModel<Product>({
    nextId: () => ++productIdCounter,
    createWithDefaults: (id) => ({ id, name: '', price: 0, category: '' }),
    selectId: (state) => state.id,
});

const useProductList = createListHook(productFactory);

function ProductList() {
    const [page, setPage] = useState(1);
    const list = useProductList();

    return (
        <div>
            {list.items.map(item => (
                <div key={item.id}>{item.name} - ${item.price}</div>
            ))}
            <span>{list.totalCount} total</span>
        </div>
    );
}

Persistence

Persistence Demo

State persists to localStorage and survives component remount and page refresh

Persisted Counter

0

Last updated: 4:25:11 PM

State auto-saves to localStorage via saveOnChange. The persist callback writes data, and loadFromPersist restores it on creation. Try refreshing the page or unmounting below.

localStorage Inspector

Key: fluxo-ui-demo-persist-counter
Value: (empty)
import { createModel, createItemHook } from 'fluxo-ui/store';

interface CounterState {
    id: string;
    count: number;
    label: string;
    lastUpdated: number;
}

// persist: custom function called whenever state is saved
// loadFromPersist: called once on store creation to restore state
// saveOnChange: auto-saves (debounced 500ms) on every setState call

const counterFactory = createModel<CounterState>({
    selectId: (state) => state.id,
    createWithDefaults: (id) => ({
        id, count: 0, label: 'Persisted Counter', lastUpdated: Date.now(),
    }),
    persist: (data) => {
        localStorage.setItem('my-counter', JSON.stringify(data));
    },
    loadFromPersist: () => {
        const stored = localStorage.getItem('my-counter');
        return stored ? JSON.parse(stored) : undefined;
    },
    saveOnChange: true,
});

const store = counterFactory.create({ id: 'main' });
const useCounter = createItemHook(store);

function Counter() {
    const state = useCounter();
    return (
        <div>
            <p>Count: {state.count}</p>
            <Button label="+"
                onClick={() => store.setState({
                    count: state.count + 1,
                    lastUpdated: Date.now(),
                })} />
            <Button label="Refresh Page"
                onClick={() => window.location.reload()} />
        </div>
    );
}

Validation

Validation

Built-in validation with real-time error feedback and field-level indicators

0/3 required fields valid
import { createModel } from 'fluxo-ui/store';

interface ContactForm {
    id: string;
    name: string;
    email: string;
    phone: string;
    message: string;
}

const contactFactory = createModel<ContactForm>({
    selectId: (state) => state.id,
    createWithDefaults: (id) => ({
        id, name: '', email: '', phone: '', message: '',
    }),
    validate: (state) => {
        const errors: Partial<Record<keyof ContactForm, string>> = {};
        if (!state.name.trim()) errors.name = 'Name is required';
        else if (state.name.trim().length < 2)
            errors.name = 'Name must be at least 2 characters';

        if (!state.email.trim()) errors.email = 'Email is required';
        else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(state.email))
            errors.email = 'Invalid email';

        if (state.phone && !/^\+?[\d\s-]{7,15}$/.test(state.phone))
            errors.phone = 'Invalid phone number';

        if (!state.message.trim()) errors.message = 'Message is required';
        else if (state.message.trim().length < 10)
            errors.message = 'At least 10 characters';

        return Object.keys(errors).length > 0 ? errors : undefined;
    },
    validateBehavior: 'change',
});

const store = contactFactory.create({ id: 'form-1' });
const useForm = createItemHook(store);

function ContactFormUI() {
    const form = useForm();
    const [touched, setTouched] = useState(new Set());
    const errors = useMemo(() => { /* derive from form state */ }, [form]);

    return (
        <form>
            <TextInput value={form.name}
                onChange={(e) => store.setState({ name: e.value })}
                onBlur={() => setTouched(prev => new Set(prev).add('name'))}
                invalid={touched.has('name') && !!errors.name} />
            {touched.has('name') && errors.name &&
                <p className="error">{errors.name}</p>}
            {/* ... other fields */}
        </form>
    );
}

Combined Demo

Task Manager

Realistic composite example: CRUD, validation, persistence, list management, and status tracking

New Task

Priority:
No tasks yet. Create one above to get started.
import { createModel, createItemHook } from 'fluxo-ui/store';

type TaskStatus = 'todo' | 'in-progress' | 'done';
type TaskPriority = 'low' | 'medium' | 'high';

interface Task {
    id: number;
    title: string;
    description: string;
    status: TaskStatus;
    priority: TaskPriority;
    createdAt: number;
    completedAt: number | null;
}

const taskFactory = createModel<Task>({
    nextId: () => ++taskIdCounter,
    createWithDefaults: (id) => ({
        id, title: '', description: '', status: 'todo',
        priority: 'medium', createdAt: Date.now(), completedAt: null,
    }),
    selectId: (state) => state.id,
    validate: (state) => {
        const errors = {};
        if (!state.title.trim()) errors.title = 'Title is required';
        return Object.keys(errors).length > 0 ? errors : undefined;
    },
    validateBehavior: 'change',
});

// Create with validation
const store = taskFactory.create({ title: 'Build feature' });
store.setState({ status: 'in-progress' });
store.setState({ status: 'done', completedAt: Date.now() });

// List with pagination & sorting
const list = taskFactory.list({ itemsPerPage: 50, sortBy: 'createdAt' });

// Manual persistence across all tasks
const allTasks = list.items;
localStorage.setItem('tasks', JSON.stringify(allTasks));

Import

import { createModel, createItemHook, createListHook } from 'fluxo-ui/store';
import type { ModelConfig, ModelFactory, ModelStore, ListState } from 'fluxo-ui/store';

API Reference

createWithDefaults
(id: any) => T"-"

Factory function returning default state for new model instances

selectId
(state: T) => any"-"

Extract the unique identifier from the model state

nextId
() => any"-"

Generate a new unique ID when creating models

loadItem
(id: any) => Promise<T>"-"

Async function to load a single item by ID from a remote source

loadItems
(options: PageOptions) => Promise<T[]>"-"

Async function to load a paginated list of items

onCreate
(data: T, options: ChangeOptions<T>) => Promise<void>"-"

Handler called when saving a new model (no existing ID)

onUpdate
(data: T, options: ChangeOptions<T>) => Promise<void>"-"

Handler called when saving an existing model

onDelete
(data: T) => Promise<void>"-"

Handler called when deleting a model

validate
(state: T) => errors | undefined"-"

Validation function returning field-level errors or undefined if valid

validateBehavior
'change' | 'save'"-"

When to run validation: on every state change or only on save

persist
boolean | 'local' | 'session' | ((store: T) => void)"false"

Enable state persistence to localStorage, sessionStorage, or a custom function

loadFromPersist
() => T | undefined"-"

Custom function to load persisted state on initialization

saveOnChange
boolean"false"

Automatically save (debounced 500ms) whenever state changes

Features

Domain Model Factory

Create typed model factories with createModel that produce individually managed store instances for each entity

Built-in CRUD

Each model instance exposes save(), delete(), destroy(), and refresh() methods backed by configurable async handlers

Pagination & Sorting

list() provides client-side or server-side pagination and multi-field sorting out of the box

Validation

Attach a validate function that runs on change or save, returning field-level error maps for form-style error display

Persistence

Optionally persist model state to localStorage or sessionStorage, or provide a custom persistence handler

React Hooks

createItemHook and createListHook provide reactive bindings so components re-render on relevant state changes