Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Generate React components that render FOSMVVM ViewModels. Scaffolds ViewModelView pattern with hooks, loading states, and TypeScript types.
.claude/skills/fosmvvm-react-view-generator/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-08 | ✗→✓ | ▲ Improved | — | — |
| case-03 | ✗→✓ | ▲ Improved | — | — |
| case-02 | ✗→✓ | ▲ Improved | — | — |
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-12 | ✗→✓ | ▲ Improved | — | — |
Generate React components that render FOSMVVM ViewModels.
> For full architecture context, see FOSMVVMArchitecture.md | OpenClaw reference
In FOSMVVM, React components are thin rendering layers that display ViewModels:
┌─────────────────────────────────────────────────────────────┐
│ ViewModelView Pattern │
├─────────────────────────────────────────────────────────────┤
│ │
│ ViewModel (Data) React Component │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ title: String │────►│ <h1>{vm.title} │ │
│ │ items: [Item] │────►│ {vm.items.map()} │ │
│ │ isEnabled: Bool │────►│ disabled={!...} │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ ServerRequest (Actions) │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ processRequest() │◄────│ <Component.bind │ │
│ │ │ │ requestType={} │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘Key principle: Components don't transform or compute data. They render what the ViewModel provides.
The component filename should match the ViewModel it renders.
src/
viewmodels/
{Feature}ViewModel.js ←──┐
{Entity}CardViewModel.js ←──┼── Same names
│
components/ │
{Feature}/ │
{Feature}View.jsx ────┤ (renders {Feature}ViewModel)
{Entity}CardView.jsx ────┘ (renders {Entity}CardViewModel)This alignment provides:
This skill generates tests FIRST, implementation SECOND in a single invocation:
1. Reference ViewModel and ServerRequest details from conversation context
2. Generate .test.js file → Tests FAIL (no implementation yet)
3. Generate .jsx file → Tests PASS
4. Verify completeness (both files exist)
5. User runs `npm test` → All tests pass ✓Context-aware: Skill references conversation understanding of requirements. No file parsing or Q&A needed.
Every component is wrapped with viewModelComponent():
jsxconst MyView = FOSMVVM.viewModelComponent(({ viewModel }) => { return <div>{viewModel.title}</div>; }); export default MyView;
Required:
FOSMVVM.viewModelComponent() from global namespace (loaded via script tag){ viewModel } prop<script> tagsParent components use .bind() to invoke ServerRequests:
jsx// Parent component function Dashboard() { return ( <div> <TaskList.bind({ requestType: 'GetTasksRequest', params: { status: 'active' } }) /> </div> ); }
The .bind() pattern:
requestType and paramsError ViewModels are rendered like any other ViewModel:
jsxconst TaskCard = FOSMVVM.viewModelComponent(({ viewModel }) => { // Handle error ViewModels if (viewModel.errorType === 'NotFoundError') { return ( <div className="error"> <p>{viewModel.message}</p> <p>{viewModel.suggestedAction}</p> </div> ); } if (viewModel.errorType === 'ValidationError') { return ( <div className="validation-error"> <h3>{viewModel.title}</h3> <ul> {viewModel.errors.map(err => ( <li key={err.field}>{err.message}</li> ))} </ul> </div> ); } // Render success ViewModel return ( <div className="task-card"> <h3>{viewModel.title}</h3> <p>{viewModel.description}</p> </div> ); });
Key principles:
errorType propertyUse navigation intents, not hardcoded paths:
jsx// FOSMVVM utilities loaded via <script> tag, available on global namespace // ❌ NEVER <a href="/tasks/123">View Task</a> // ✅ ALWAYS <FOSMVVM.Link to={{ intent: 'viewTask', id: viewModel.id }}> {viewModel.linkText} </FOSMVVM.Link>
Navigation patterns:
FOSMVVM.Link from global namespace (loaded via script tag)intent property, not hardcoded pathsComponents that just render data (no user interactions):
jsxconst InfoCard = FOSMVVM.viewModelComponent(({ viewModel }) => { return ( <div className="info-card"> <h2>{viewModel.title}</h2> <p>{viewModel.description}</p> {viewModel.isActive && ( <span className="badge">{viewModel.activeLabel}</span> )} </div> ); }); export default InfoCard;
Characteristics:
Components with user actions that trigger ServerRequests:
jsxconst ActionCard = FOSMVVM.viewModelComponent(({ viewModel }) => { return ( <div className="action-card"> <h2>{viewModel.title}</h2> <p>{viewModel.description}</p> <div className="actions"> <button onClick={() => viewModel.operations.performAction()} disabled={!viewModel.canPerformAction} > {viewModel.actionLabel} </button> <button onClick={() => viewModel.operations.cancel()}> {viewModel.cancelLabel} </button> </div> </div> ); }); export default ActionCard;
Components that render collections:
jsxconst TaskList = FOSMVVM.viewModelComponent(({ viewModel }) => { if (viewModel.isEmpty) { return <div className="empty">{viewModel.emptyMessage}</div>; } return ( <div className="task-list"> <h2>{viewModel.title}</h2> <p>{viewModel.totalCount}</p> {viewModel.tasks.map(task => ( <TaskCard.bind({ requestType: 'GetTaskRequest', params: { id: task.id } }) /> ))} </div> ); }); export default TaskList;
Components with validated input fields:
jsxconst SignInForm = FOSMVVM.viewModelComponent(({ viewModel }) => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [errors, setErrors] = useState({}); const handleSubmit = async (e) => { e.preventDefault(); const result = await viewModel.operations.submit({ email, password }); if (result.validationErrors) { setErrors(result.validationErrors); } }; return ( <form onSubmit={handleSubmit}> <div> <label>{viewModel.emailLabel}</label> <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder={viewModel.emailPlaceholder} /> {errors.email && <span className="error">{errors.email}</span>} </div> <div> <label>{viewModel.passwordLabel}</label> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder={viewModel.passwordPlaceholder} /> {errors.password && <span className="error">{errors.password}</span>} </div> <button type="submit" disabled={viewModel.submitDisabled}> {viewModel.submitLabel} </button> </form> ); }); export default SignInForm;
Two files per invocation:
| File | Location | Purpose | |------|----------|---------| | {ViewName}View.test.js | src/components/{Feature}/ | Jest + React Testing Library tests | | {ViewName}View.jsx | src/components/{Feature}/ | React component |
Test file generated FIRST (tests fail initially) Implementation file generated SECOND (tests pass)
Note: The corresponding ViewModel and ServerRequest should already exist (use other FOSMVVM generator skills).
| Placeholder | Description | Example | |-------------|-------------|---------| | {ViewName} | View name (without "View" suffix) | TaskList, SignIn | | {Feature} | Feature/module grouping | Tasks, Auth |
This skill references conversation context to determine component structure:
From conversation context, the skill identifies:
Based on component type, generates .test.js with:
Generates .jsx following patterns:
viewModelComponent wrapper.bind() calls (if container)Skill references information from:
Check:
.test.js file exists.jsx file existsFOSMVVM.viewModelComponent() wrapperjsx// ❌ BAD - Component is transforming data const TaskCard = FOSMVVM.viewModelComponent(({ viewModel }) => { const daysLeft = Math.ceil((viewModel.dueDate - Date.now()) / 86400000); return <span>{daysLeft} days remaining</span>; }); // ✅ GOOD - ViewModel provides shaped result const TaskCard = FOSMVVM.viewModelComponent(({ viewModel }) => { return <span>{viewModel.daysRemainingText}</span>; });
jsx// ❌ BAD - Component making HTTP requests const TaskCard = FOSMVVM.viewModelComponent(({ viewModel }) => { const [data, setData] = useState(null); useEffect(() => { fetch(`/api/tasks/${viewModel.id}`) .then(r => r.json()) .then(setData); }, [viewModel.id]); return <div>{data?.title}</div>; }); // ✅ GOOD - Parent uses .bind() to invoke ServerRequest <TaskCard.bind({ requestType: 'GetTaskRequest', params: { id: taskId } }) />
jsx// ❌ BAD - Generic error handling const TaskCard = FOSMVVM.viewModelComponent(({ viewModel }) => { if (viewModel.error) { return <div>Error: {viewModel.error.message}</div>; } return <div>{viewModel.title}</div>; }); // ✅ GOOD - Specific error ViewModels const TaskCard = FOSMVVM.viewModelComponent(({ viewModel }) => { if (viewModel.errorType === 'NotFoundError') { return ( <div className="not-found"> <h3>{viewModel.errorTitle}</h3> <p>{viewModel.errorMessage}</p> <p>{viewModel.suggestedAction}</p> </div> ); } if (viewModel.errorType === 'ValidationError') { return ( <div className="validation-error"> <h3>{viewModel.errorTitle}</h3> <ul> {viewModel.validationErrors.map(err => ( <li key={err.field}>{err.message}</li> ))} </ul> </div> ); } return <div>{viewModel.title}</div>; });
jsx// ❌ BAD - Hardcoded URLs const TaskRow = FOSMVVM.viewModelComponent(({ viewModel }) => { return ( <div> <a href={`/tasks/${viewModel.id}`}>{viewModel.title}</a> </div> ); }); // ✅ GOOD - Navigation intents // FOSMVVM utilities loaded via <script> tag, available on global namespace const TaskRow = FOSMVVM.viewModelComponent(({ viewModel }) => { return ( <div> <FOSMVVM.Link to={{ intent: 'viewTask', id: viewModel.id }}> {viewModel.title} </FOSMVVM.Link> </div> ); });
src/components/
├── {Feature}/
│ ├── {Feature}View.jsx # Full page → {Feature}ViewModel
│ ├── {Feature}View.test.js # Tests for {Feature}View
│ ├── {Entity}CardView.jsx # Child component → {Entity}CardViewModel
│ ├── {Entity}CardView.test.js # Tests for {Entity}CardView
│ └── {Entity}RowView.jsx # Child component → {Entity}RowViewModel
├── Shared/
│ ├── HeaderView.jsx # Shared components
│ └── FooterView.jsxjsx// ❌ BAD - Component is transforming data const UserCard = FOSMVVM.viewModelComponent(({ viewModel }) => { return <div>{viewModel.firstName} {viewModel.lastName}</div>; }); // ✅ GOOD - ViewModel provides shaped result const UserCard = FOSMVVM.viewModelComponent(({ viewModel }) => { return <div>{viewModel.fullName}</div>; });
jsx// ❌ BAD - fetch() call in component const TaskList = FOSMVVM.viewModelComponent(({ viewModel }) => { const [tasks, setTasks] = useState([]); useEffect(() => { fetch('/api/tasks').then(r => r.json()).then(setTasks); }, []); return <div>{tasks.map(t => <div key={t.id}>{t.title}</div>)}</div>; }); // ✅ GOOD - Parent uses .bind() with ServerRequest <TaskList.bind({ requestType: 'GetTasksRequest', params: {} }) />
jsx// ❌ BAD - Not localizable const TaskCard = FOSMVVM.viewModelComponent(({ viewModel }) => { return ( <button onClick={viewModel.operations.submit}> Submit </button> ); }); // ✅ GOOD - ViewModel provides localized text const TaskCard = FOSMVVM.viewModelComponent(({ viewModel }) => { return ( <button onClick={viewModel.operations.submit}> {viewModel.submitLabel} </button> ); });
jsx// ❌ BAD - Hardcoded path const TaskRow = FOSMVVM.viewModelComponent(({ viewModel }) => { return <a href={`/tasks/${viewModel.id}`}>{viewModel.title}</a>; }); // ✅ GOOD - Navigation intent // FOSMVVM utilities loaded via <script> tag, available on global namespace const TaskRow = FOSMVVM.viewModelComponent(({ viewModel }) => { return ( <FOSMVVM.Link to={{ intent: 'viewTask', id: viewModel.id }}> {viewModel.title} </FOSMVVM.Link> ); });
jsx// ❌ BAD - Missing viewModelComponent() wrapper const TaskCard = ({ viewModel }) => { return <div>{viewModel.title}</div>; }; export default TaskCard; // ✅ GOOD - Wrapped with viewModelComponent() const TaskCard = FOSMVVM.viewModelComponent(({ viewModel }) => { return <div>{viewModel.title}</div>; }); export default TaskCard;
// ❌ BAD - Filename doesn't match ViewModel
ViewModel: TaskListViewModel
Component: Tasks.jsx
// ✅ GOOD - Aligned names
ViewModel: TaskListViewModel
Component: TaskListView.jsxSee reference.md for complete file templates.
| Concept | Convention | Example | |---------|------------|---------| | Component file | {Name}View.jsx | TaskListView.jsx, SignInView.jsx | | Test file | {Name}View.test.js | TaskListView.test.js | | Component function | {Name}View | TaskListView, SignInView | | ViewModel prop | viewModel | Always viewModel |
javascriptit('renders task card with ViewModel', () => { const viewModel = { title: 'Test Task', description: 'Test Description', dueDate: 'Jan 30, 2026' }; render(<TaskCard viewModel={viewModel} />); expect(screen.getByText('Test Task')).toBeInTheDocument(); expect(screen.getByText('Test Description')).toBeInTheDocument(); });
javascriptit('renders NotFoundViewModel', () => { const viewModel = { errorType: 'NotFoundError', errorTitle: 'Task Not Found', errorMessage: 'The task you requested does not exist', suggestedAction: 'Try searching for a different task' }; render(<TaskCard viewModel={viewModel} />); expect(screen.getByText('Task Not Found')).toBeInTheDocument(); expect(screen.getByText(/does not exist/)).toBeInTheDocument(); });
javascriptit('calls operation when button clicked', () => { const mockOperation = jest.fn(); const viewModel = { title: 'Test Task', submitLabel: 'Complete Task', operations: { complete: mockOperation } }; render(<TaskCard viewModel={viewModel} />); fireEvent.click(screen.getByText('Complete Task')); expect(mockOperation).toHaveBeenCalled(); });
Invocation:
bash/fosmvvm-react-view-generator
Prerequisites:
Output:
{ComponentName}.test.js - Generated FIRST (tests fail){ComponentName}.jsx - Generated SECOND (tests pass)Workflow integration: This skill is typically used after discussing requirements or reading specification files. The skill references that context automatically—no file paths or Q&A needed.
| Version | Date | Changes | |---------|------|---------| | 1.0 | 2026-01-23 | Initial skill for React view generation based on Kairos requirements |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-08 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +59 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.