reactive-ui-patterns
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.
- Category: discovered
- Tags: github, community
- Version: 1.0.0
- Author: walid
- Downloads: 0
- Updated: 2025-11-22
Download the latest version (v1.0.0) from this page.
SKILL.md
---
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:
1. **State Tracker Plugin** - Manages application state transitions
2. **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:
1. Update application state via `api:setState`
2. Listen for state transitions via `state:transition` events
3. 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 fetch
- `fx-method` - HTTP method (GET, POST, etc.)
- `fx-target` - Where to put the response
- `fx-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
```javascript
// 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
```javascript
// 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
```javascript
// 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
```javascript
// 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
```javascript
// 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
```javascript
button.addEventListener('click', () => {
api:setState({ state: 'loading' }) // NO! Creates tight coupling
})
```
### ✅ Do: Use fx- attributes and listen to fx:events
```javascript
// 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
```javascript
api:injectFxHtml({
html: '<button onclick="doSomething()">Click</button>' // NO!
})
```
### ✅ Do: Use fx-action for behavior
```javascript
api:injectFxHtml({
html: '<button fx-action="/api/action">Click</button>' // YES!
})
```
### ❌ Don't: Create circular state dependencies
```javascript
ctx.on('state:entered:loading', () => {
api:setState({ state: 'loaded' }) // Might trigger again!
})
```
### ✅ Do: Use conditional state transitions
```javascript
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:
```javascript
// 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 state
- `api:setState` - Transition to new state
- `api:waitForState` - Wait for specific state
- `api:registerStateSchema` - Define state machine
- `api:getStateHistory` - Debug state transitions
### Fixi-Agent Tools
- `api:injectFxHtml` - Inject HTML with fx- attributes
- `api:readDom` - Read current DOM state
- `api:triggerFxElement` - Activate fx-action elements
- `api:getFxDocumentation` - Learn fx- attribute system
### Events to Listen For
- `state:transition` - Any state change
- `state:entered:{stateName}` - Entering specific state
- `fx:before` - Before AJAX request
- `fx:after` - After successful AJAX
- `fx: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
1. **Track state history**:
```javascript
const history = await api:getStateHistory({ limit: 20 })
console.log('Recent transitions:', history)
```
2. **Listen to all state transitions**:
```javascript
ctx.on('state:transition', (event) => {
console.log(`State: ${event.from} → ${event.to}`, event.data)
})
```
3. **Debug fx- events**:
```javascript
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))
```
4. **Read current DOM**:
```javascript
const dom = await api:readDom({ selector: '#app', property: 'outerHTML' })
console.log('Current HTML:', dom.value)
```
## Summary
Reactive UI with FixiPlug = **State + Declarative DOM**
1. Define your state machine
2. Inject HTML with fx- attributes
3. Listen to state transitions
4. Update UI in response to state
5. 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)