cuel-x
v0.0.5
Published
Tiny, powerful state management for cuel
Maintainers
Readme
Cuel-X
A lightweight state management library for cuel DOM data binding components. Cuel-X provides Redux-compatible state management with proxy-based APIs for automatic validation and type safety. Complete implementation in <1000 SLoC.
Quick Start
import {setup} from 'cuel-x';
// Initialize with initial state
const {actions, connect, register, state} = setup({
todos: {},
filter: 'all',
newTodo: ''
});
// Register actions, reducers, and selectors
register.todo('add', src, (state, {payload}, nextId) => {
state[nextId] = {id: nextId, text: payload.newTodo, completed: false};
}, state.nextId);
// Connect to components
cuel('todo-input', {}, {
...connect(actions.todo.add, state.newTodo),
template: `<input .value=newTodo !click=actions.todo.add>`
});Core Concepts
Proxy-Based Architecture
Cuel-X uses proxy objects instead of strings for type-safe state management:
- State Proxy: Access state properties with automatic validation
- Action Proxy: Type-safe action dispatching with payload parsing
- Registry Proxy: Register actions, reducers, and selectors with structure validation
Redux Compatibility
100% Redux compatible with built-in implementation:
- Predictable state updates
- Immutable state (via immer-like patterns)
- Middleware support
- Async thunks
- Action creators and reducers
API Reference
setup(initialState)
Initializes the complete cuel-x system with default configuration.
Parameters:
initialState(Object): Initial state structure
Returns:
{
actions: ActionProxy, // Action dispatchers
connect: Function, // Component connection function
register: RegistryProxy, // Registration API
state: StateProxy // State access proxy
}State Proxy (state)
Access state properties and selectors with automatic validation:
// Direct state access
state.todos // Access todos state
state.filter // Access filter state
// Selector access
state.filteredTodos // Computed selector
state.stats // Stats selectorAction Proxy (actions)
Dispatch actions with type safety and automatic payload parsing:
// Basic action creator - note, you don't get the creator but a proxy,
// this is an API for reducer registry and connecting to cuel-DOM
actions.todo.addRegistry Proxy (register)
Register actions, reducers, middleware, domains, and selectors:
// Register domain
register.todo('todo');
// Register action and reducer
register.todo.add('addTodo', src, (state, {payload}, nextId) => {
state[nextId] = {id: nextId, text: payload, completed: false};
}, state.nextId);
// Register selector
register.filteredTodos((filter, todos) => {
return Object.values(todos).filter(/* filter logic */);
}, state.filter, state.todo);
// Register middleware
register(myMiddleware);Action Creators
Cuel-X provides built-in action creators for common patterns:
import {action, asyncAction, data, src, form, asyncData, asyncSrc, asyncForm} from 'cuel-x';
// Sync actions
const addTodo = action(type); // Basic action
const setData = data(type); // Extract payload.data
const setSrc = src(type); // Extract payload.src
const setForm = form(type); // Extract FormData
// Async actions (with pending/fulfilled/rejected)
const fetchTodos = asyncAction(handler)(type); // Basic async
const fetchData = asyncData( handler)(type); // Extract payload.data
const fetchSrc = asyncSrc( handler)(type); // Extract payload.src
const fetchForm = asyncForm( handler)(type); // Extract FormDataNote, that you usually don't pass type to the action creators, as they are automatically initialized by the registry.
Usage Examples
// Basic action creator
register.todo.add('add', action(), (state, {payload}) => {
// Handle action
});
// Data extraction (for form submissions)
register.user.update('update', data(), (state, {payload}) => {
const userData = payload.data; // Already extracted
state.user = userData;
});
// Source element extraction
register.todo.toggle('toggle', src(), (state, {payload}) => {
const element = payload.src; // DOM element
const id = parseInt(element.dataset.id);
state.todos[id].completed = !state.todos[id].completed;
});
// Async action with thunk
register.todo.fetch('fetch', asyncData(async function*() {
yield {type: 'todo/fetch/pending'};
try {
const todos = await api.fetchTodos();
yield {type: 'todo/fetch/fulfilled', payload: todos};
} catch (error) {
yield {type: 'todo/fetch/rejected', payload: error};
}
});Reducers and Selectors
Reducers
Reducers modify state based on actions:
// Basic reducer
register.todo.add('add', src, (state, {payload}, nextId) => {
const text = payload.newTodo.trim();
if (!text) return state;
state[nextId] = {id: nextId, text, completed: false};
}, state.nextId);
// Multiple action types
register.todo.update(['add', 'toggle'], src, (state, {payload}) => {
// Handle multiple action types
});
// With dependencies
register.todo.delete('delete', src, (state, {payload}) => {
delete state[payload.id];
}, state.someDependency);Selectors
Selectors compute derived state:
// Basic selector
register.filteredTodos((filter, todos) => {
todos = Object.values(todos);
switch (filter) {
case 'active': return todos.filter(t => !t.completed);
case 'completed': return todos.filter(t => t.completed);
default: return todos;
}
}, state.filter, state.todo);
// Complex selector
register.stats(todos => {
todos = Object.values(todos);
const total = todos.length;
const completed = todos.filter(t => t.completed).length;
return {total, completed, active: total - completed};
}, state.todo);Component Connection
connect(...)
Connects state and actions to cuel components:
// Connect state and actions
cuel('todo-input', {}, {
...connect(
actions.todo.add, // Action dispatcher
state.newTodo, // State binding
state.filteredTodos // Selector binding
),
template: `
<input .value=newTodo !click=add>
<ul><li is=todo-item *=filteredTodos</li></ul>
`
});
// Multiple connections
cuel('todo-stats', {}, {
...connect(
state.stats.active,
state.stats.active('deep.active2', 'deep.active3')
),
template: '{{active}} {{deep.active2}}{{deep.active3}}'
});Connection Patterns
// Action only
connect(actions.todo.add)
// State only
connect(state.newTodo)
// Selector with nested properties
connect(state.stats.active('deep.active2', 'deep.active3'))
// Mixed
connect(actions.todo.add, state.newTodo, state.filteredTodos)Advanced Features
Middleware
Register middleware for cross-cutting concerns:
const logger = (action, store) => {
console.log('Dispatching:', action);
yield; // Yielding undefined continues the middleware chain
console.log('After dispatch', store.getState());
};
register(logger);Yielding anything else than undefined stops the chain, no dispatch will happen. Yielding a Promise will pause execution until the promise resolves, but the chain is stopped. Yielding an action (an object with a type-property) will dispatch that action from the top of the chain. Same for a Promise that resolves to an action.
Domains
Organize actions with domains:
register.todo('todo'); // Set domain
register.todo.add('add', src, reducer); // Creates 'todo/add'
register.user('user'); // Different domain
register.user.update('update', data, reducer); // Creates 'user/update'Custom Action Processing
Create custom action creators:
register.custom('action', formAction((parsedFormData, store) =>
({type, payload: myCustomProcessing(parsedFormData, store)})));Browser Import
<script type="importmap">
{
"imports": {
"cuel/": "./node_modules/cuel/",
"cuel-x": "./node_modules/cuel-x/index.mjs"
}
}
</script>License
MIT
