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
| Name | Category | Price |
|---|---|---|
| No products. Add some above. | ||
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
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
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
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
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
selectId(state: T) => any"-"Extract the unique identifier from the model state
nextId() => any"-"Generate a new unique ID when creating models
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
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
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)
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
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
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
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
validateBehavior'change' | 'save'"-"When to run validation: on every state change or only on save
persistboolean | 'local' | 'session' | ((store: T) => void)"false"Enable state persistence to localStorage, sessionStorage, or a custom function
persistboolean | '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
loadFromPersist() => T | undefined"-"Custom function to load persisted state on initialization
saveOnChangeboolean"false"Automatically save (debounced 500ms) whenever state changes
saveOnChangeboolean"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