Build Your First Construct
Let's build a simple todo list to learn the Construct patterns.
1. Initialize the Project
Use the SDK to create a new Construct:
$ npx @useworkapp/construct-sdk init "Todo List" $ cd todo-list $ npm install $ npm run dev
This creates a complete Vite + React + Tailwind project with shadcn/ui components pre-installed. Open http://localhost:5173 to see the starter app.
2. Build Your App
Open src/App.tsx and replace it with:
Imports
"use client";
import { useState } from "react";
import { useConstructData } from "@useworkapp/construct-sdk";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { Plus, Trash2 } from "lucide-react";Use useConstructData from the SDK for persistence. Import UI components from shadcn/ui and icons from lucide-react.
Types
interface Todo {
id: string;
text: string;
completed: boolean;
}State with useConstructData
export default function App() {
const [todos, setTodos, { isLoading }] = useConstructData<Todo[]>("todos", []);
const [input, setInput] = useState("");
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
Loading...
</div>
);
}useConstructData handles IndexedDB storage automatically. Always handle the loading state before rendering data-dependent UI.
Actions
const addTodo = () => {
if (!input.trim()) return;
setTodos([
...todos,
{ id: crypto.randomUUID(), text: input.trim(), completed: false }
]);
setInput("");
};
const toggleTodo = (id: string) => {
setTodos(todos.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
));
};
const deleteTodo = (id: string) => {
setTodos(todos.filter(t => t.id !== id));
};JSX
return (
<div className="min-h-screen bg-background p-4 sm:p-6">
<Card className="mx-auto max-w-md">
<CardHeader>
<CardTitle>Todo List</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Add form */}
<form
onSubmit={(e) => { e.preventDefault(); addTodo(); }}
className="flex gap-2"
>
<Input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Add a task..."
/>
<Button type="submit" size="icon" aria-label="Add task">
<Plus className="h-4 w-4" />
</Button>
</form>
{/* Todo list */}
<div className="space-y-2">
{todos.map((todo) => (
<div
key={todo.id}
className="flex items-center gap-3 p-2 border rounded"
>
<Checkbox
id={todo.id}
checked={todo.completed}
onCheckedChange={() => toggleTodo(todo.id)}
/>
<Label
htmlFor={todo.id}
className={`flex-1 ${todo.completed ? "line-through text-muted-foreground" : ""}`}
>
{todo.text}
</Label>
<Button
variant="ghost"
size="icon"
onClick={() => deleteTodo(todo.id)}
aria-label="Delete task"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
</div>
{todos.length === 0 && (
<p className="text-center text-muted-foreground text-sm">
No tasks yet. Add one above!
</p>
)}
</CardContent>
</Card>
</div>
);3. Validate
Run the validator to ensure your Construct meets all requirements:
$ npx @useworkapp/construct-sdk validate Validating construct... ✓ App.tsx exists ✓ Default export found ✓ Using allowed imports only ✓ Data persistence with useConstructData ✓ Accessibility: aria-label on icon buttons ✓ Accessibility: Labels connected to inputs ✓ Construct passes all validations!
4. Publish & Submit
Once validated, publish and submit for review:
# Login (first time only) $ npx @useworkapp/construct-sdk login # Publish to Work platform $ npx @useworkapp/construct-sdk publish ✓ Construct published! ID: abc123 # Submit for review $ npx @useworkapp/construct-sdk submit abc123 ✓ Submitted for review. We'll notify you within 48 hours.
Key Patterns to Remember
Use useConstructData for persistence
Handles IndexedDB, loading states, and optional cloud sync automatically.
Handle loading states
Always check isLoading before rendering data-dependent UI.
Add accessibility attributes
Icon buttons need aria-label. Connect labels to inputs with id/htmlFor.
Use Tailwind for styling
Use utility classes, not inline styles. Include responsive padding.
Common Mistakes to Avoid
Don't use fetch or axios
Constructs run in a sandbox without network access. Use IndexedDB for storage.
Don't skip the loading state
Data loads async from IndexedDB. Show a loading indicator while isLoading is true.
Don't use inline styles
Use Tailwind classes instead of style={{}}.
Don't forget accessibility
Icon buttons need aria-label. Form inputs need associated labels.