| name | reactive-ui-patterns |
| description | Build reactive user interfaces by orchestrating FixiPlug state management and DOM manipulation. Use when creating dynamic interfaces that respond to application state changes, coordinate async operations, or implement event-driven UI updates. |
| tags | ui, reactive, patterns, state-management, dom-manipulation, workflow, ajax, best-practices |
| version | 1.0.0 |
| level | intermediate |
| author | FixiPlug Team |
| references | stateTrackerPlugin, fixiAgentPlugin, introspectionPlugin |
Reactive UI Patterns with FixiPlug
Overview
This skill teaches you to build reactive user interfaces by combining two FixiPlug plugins:
- State Tracker Plugin - Manages application state transitions
- Fixi-Agent Plugin - Provides declarative DOM manipulation with fx- attributes
The key pattern: state drives UI updates, creating a unidirectional data flow similar to modern reactive frameworks.
Core Concepts
State-Driven UI
Instead of directly manipulating the DOM, you:
- Update application state via
api:setState - Listen for state transitions via
state:transitionevents - Inject/update UI in response to state changes via
api:injectFxHtml
This separates business logic (state) from presentation (DOM).
Declarative AJAX with fx- Attributes
The fixi-agent plugin allows you to inject HTML with special fx- attributes that handle AJAX requests declaratively:
fx-action- The URL to fetchfx-method- HTTP method (GET, POST, etc.)fx-target- Where to put the responsefx-swap- How to swap (innerHTML, outerHTML, etc.)fx-trigger- What event triggers the request
Pattern 1: State-Driven Loading Indicator
Use Case: Show/hide loading indicator based on async operation state
// Step 1: Define state machine
await api:registerStateSchema({
states: ['idle', 'loading', 'success', 'error'],
transitions: {
idle: ['loading'],
loading: ['success', 'error'],
success: ['idle'],
error: ['idle']
},
initial: 'idle'
})
// Step 2: Create UI container
await api:injectFxHtml({
html: '<div id="status-container"></div>',
selector: '#app',
position: 'beforeend'
})
// Step 3: Listen for state changes (in a plugin)
ctx.on('state:entered:loading', () => {
api:injectFxHtml({
html: '<div class="spinner">Loading...</div>',
selector: '#status-container',
position: 'innerHTML'
})
})
ctx.on('state:entered:success', (event) => {
api:injectFxHtml({
html: `<div class="success">${event.data.message}</div>`,
selector: '#status-container',
position: 'innerHTML'
})
})
// Step 4: Trigger async operation
await api:setState({ state: 'loading' })
// ... perform async work ...
await api:setState({
state: 'success',
data: { message: 'Data loaded!' }
})
Pattern 2: Progressive Enhancement with fx-action
Use Case: Start with a basic button, enhance it with reactive behavior
// Step 1: Inject basic interactive element
await api:injectFxHtml({
html: `
<button
fx-action="/api/data"
fx-target="#result"
fx-swap="innerHTML"
fx-trigger="click">
Load Data
</button>
<div id="result"></div>
`,
selector: '#app',
position: 'beforeend'
})
// The button now:
// 1. Listens for 'click' events automatically
// 2. Fetches /api/data when clicked
// 3. Swaps the response into #result
// 4. All without writing any JavaScript event handlers!
Pattern 3: Multi-Step Workflow with State Coordination
Use Case: Coordinate multiple async steps with state tracking
// Step 1: Define workflow states
await api:registerStateSchema({
states: ['start', 'fetching-users', 'selecting-user', 'loading-details', 'complete'],
transitions: {
start: ['fetching-users'],
'fetching-users': ['selecting-user', 'error'],
'selecting-user': ['loading-details'],
'loading-details': ['complete', 'error'],
complete: ['start'],
error: ['start']
}
})
// Step 2: Start workflow
await api:setState({ state: 'fetching-users' })
// Step 3: Inject user list (server returns HTML with fx-action buttons)
await api:injectFxHtml({
html: `
<div fx-action="/api/users" fx-target="#user-list" fx-trigger="load">
<!-- Server returns: -->
<!-- <button fx-action="/api/user/1" fx-target="#details">User 1</button> -->
<!-- <button fx-action="/api/user/2" fx-target="#details">User 2</button> -->
</div>
<div id="user-list"></div>
<div id="details"></div>
`,
selector: '#app'
})
// Step 4: Listen for fx:after to advance state
ctx.on('fx:after', async (event) => {
const currentState = await api:getCurrentState()
if (event.cfg.action.includes('/api/users')) {
await api:setState({ state: 'selecting-user' })
} else if (event.cfg.action.includes('/api/user/')) {
await api:setState({ state: 'loading-details' })
}
})
// Step 5: Wait for completion
const result = await api:waitForState({
state: 'complete',
timeout: 30000
})
Pattern 4: Optimistic Updates
Use Case: Update UI immediately, roll back on error
// Step 1: Read current state
const currentData = await api:readDom({
selector: '#item-count',
property: 'textContent'
})
// Step 2: Optimistically update UI
await api:injectFxHtml({
html: `<span id="item-count">${parseInt(currentData.value) + 1}</span>`,
selector: '#item-count',
position: 'outerHTML'
})
// Step 3: Track state for rollback
await api:setState({
state: 'pending-save',
data: { previousValue: currentData.value }
})
// Step 4: Trigger actual save
await api:triggerFxElement({ selector: '#save-button' })
// Step 5: Roll back on error
ctx.on('state:entered:error', async (event) => {
if (event.data.previousValue) {
await api:injectFxHtml({
html: `<span id="item-count">${event.data.previousValue}</span>`,
selector: '#item-count',
position: 'outerHTML'
})
}
})
Pattern 5: Form Validation with State
Use Case: Coordinate client-side validation with server submission
// Step 1: Inject form with validation
await api:injectFxHtml({
html: `
<form id="user-form" fx-action="/api/users" fx-method="POST" fx-swap="none">
<input name="email" type="email" required />
<button type="submit">Submit</button>
</form>
<div id="validation-errors"></div>
`,
selector: '#app'
})
// Step 2: Validate before submission
ctx.on('fx:before', async (event) => {
if (event.cfg.action === '/api/users') {
const form = event.trigger.target.closest('form')
if (!form.checkValidity()) {
event.preventDefault() // Cancel request
await api:setState({ state: 'validation-error' })
await api:injectFxHtml({
html: '<div class="error">Please fix form errors</div>',
selector: '#validation-errors',
position: 'innerHTML'
})
}
}
})
// Step 3: Handle server response
ctx.on('fx:after', async (event) => {
if (event.cfg.action === '/api/users') {
await api:setState({ state: 'submitted' })
await api:injectFxHtml({
html: '<div class="success">Form submitted!</div>',
selector: '#validation-errors',
position: 'innerHTML'
})
}
})
Best Practices
1. Separation of Concerns
- State Logic: Use state-tracker for business logic and workflow
- UI Logic: Use fixi-agent for presentation and user interaction
- Never mix: Don't put business logic in fx- attributes
2. Unidirectional Data Flow
- State changes → UI updates (never UI changes → state changes directly)
- Listen to state transitions, not DOM events
- Use fx:events to coordinate with state
3. Error Handling
- Always handle 'error' state in your state machine
- Use fx:error event to catch AJAX failures
- Provide user feedback for all error states
4. Performance
- Use
fx-swap="innerHTML"when updating container contents - Use
fx-swap="beforeend"for appending (lists, infinite scroll) - Avoid unnecessary state transitions
- Batch state updates when possible
5. Accessibility
- Inject semantic HTML
- Provide loading indicators for async states
- Use ARIA attributes in injected content
- Test keyboard navigation
Common Pitfalls
❌ Don't: Update state from UI callbacks directly
button.addEventListener('click', () => {
api:setState({ state: 'loading' }) // NO! Creates tight coupling
})
✅ Do: Use fx- attributes and listen to fx:events
// HTML
<button fx-action="/api/data" fx-target="#result">Load</button>
// Plugin
ctx.on('fx:before', (event) => {
if (event.cfg.action === '/api/data') {
api:setState({ state: 'loading' })
}
})
❌ Don't: Inject HTML with inline event handlers
api:injectFxHtml({
html: '<button onclick="doSomething()">Click</button>' // NO!
})
✅ Do: Use fx-action for behavior
api:injectFxHtml({
html: '<button fx-action="/api/action">Click</button>' // YES!
})
❌ Don't: Create circular state dependencies
ctx.on('state:entered:loading', () => {
api:setState({ state: 'loaded' }) // Might trigger again!
})
✅ Do: Use conditional state transitions
ctx.on('state:entered:loading', async () => {
const data = await fetchData()
api:setState({ state: 'loaded', data }) // Only transition once
})
Example: Complete Todo App
Putting it all together:
// 1. Register state machine
await api:registerStateSchema({
states: ['idle', 'loading-todos', 'ready', 'adding-todo', 'deleting-todo'],
transitions: {
idle: ['loading-todos'],
'loading-todos': ['ready', 'error'],
ready: ['adding-todo', 'deleting-todo'],
'adding-todo': ['ready', 'error'],
'deleting-todo': ['ready', 'error'],
error: ['idle']
}
})
// 2. Create app structure
await api:injectFxHtml({
html: `
<div id="todo-app">
<form fx-action="/api/todos" fx-method="POST" fx-swap="none">
<input name="title" required />
<button type="submit">Add Todo</button>
</form>
<div fx-action="/api/todos" fx-target="#todo-list" fx-trigger="load"></div>
<ul id="todo-list"></ul>
</div>
`,
selector: 'body'
})
// 3. Listen for state transitions
ctx.on('state:entered:loading-todos', () => {
api:injectFxHtml({
html: '<li>Loading...</li>',
selector: '#todo-list',
position: 'innerHTML'
})
})
ctx.on('state:entered:ready', () => {
// List is automatically populated by fx-trigger="load"
})
// 4. Handle form submission
ctx.on('fx:after', async (event) => {
if (event.cfg.action === '/api/todos' && event.cfg.method === 'POST') {
// Reload list after adding
await api:triggerFxElement({ selector: '[fx-action="/api/todos"]' })
}
})
// 5. Start app
await api:setState({ state: 'loading-todos' })
Tools You'll Use
State Tracker Tools
api:getCurrentState- Check current stateapi:setState- Transition to new stateapi:waitForState- Wait for specific stateapi:registerStateSchema- Define state machineapi:getStateHistory- Debug state transitions
Fixi-Agent Tools
api:injectFxHtml- Inject HTML with fx- attributesapi:readDom- Read current DOM stateapi:triggerFxElement- Activate fx-action elementsapi:getFxDocumentation- Learn fx- attribute system
Events to Listen For
state:transition- Any state changestate:entered:{stateName}- Entering specific statefx:before- Before AJAX requestfx:after- After successful AJAXfx:error- AJAX request failed
When to Use This Pattern
✅ Good For:
- Multi-step workflows
- Form-heavy applications
- CRUD operations with loading states
- Real-time data updates
- Progressive enhancement
- Event-driven UIs
❌ Not Ideal For:
- Simple static pages
- Performance-critical animations
- Complex DOM manipulations
- Large-scale SPAs (consider a full framework)
Debugging Tips
Track state history:
const history = await api:getStateHistory({ limit: 20 }) console.log('Recent transitions:', history)Listen to all state transitions:
ctx.on('state:transition', (event) => { console.log(`State: ${event.from} → ${event.to}`, event.data) })Debug fx- events:
ctx.on('fx:before', (e) => console.log('Request:', e.cfg.action)) ctx.on('fx:after', (e) => console.log('Response:', e.cfg.text)) ctx.on('fx:error', (e) => console.error('Error:', e.error))Read current DOM:
const dom = await api:readDom({ selector: '#app', property: 'outerHTML' }) console.log('Current HTML:', dom.value)
Summary
Reactive UI with FixiPlug = State + Declarative DOM
- Define your state machine
- Inject HTML with fx- attributes
- Listen to state transitions
- Update UI in response to state
- Let fx- attributes handle user interaction
This creates a clean separation between:
- What the UI should show (state)
- How to show it (fx- attributes)
- When to update (state transitions)