Preact Mini Project — Build a Complete Preact Application
In this tutorial, you will learn about Preact Mini Project. We cover key concepts, practical examples, and best practices to help you master this topic.
Build a complete Preact application from scratch: a task management dashboard combining components, hooks, signals, routing, and forms with the 3kB framework.
In this lesson, you'll apply everything you've learned to build a real-world Preact application with multiple features and best practices.
What You'll Learn
How to architect a Preact application, combine hooks with signals for state management, implement routing, handle forms, and optimize the final build.
Why It Matters
Building a complete project solidifies your understanding. You'll see how all the pieces fit together and make architectural decisions for your own Preact projects.
Real-World Use
This project mirrors the task management interface used inside DodaTech's internal tools team, where engineers track bugs, features, and code reviews in a lightweight Preact dashboard.
flowchart TD
A[App Shell] --> B[Router]
B --> C[/ Dashboard]
B --> D[/tasks Task List]
B --> E[/tasks/new New Task]
B --> F[/tasks/:id Task Detail]
C --> G[Task Summary]
D --> H[Filterable List]
E --> I[Create Form]
F --> J[Edit Form]
style A fill:#673ab8,color:#fff
style C fill:#4a148c,color:#fff
Project Structure
task-manager/
├── src/
│ ├── main.jsx # Entry point
│ ├── App.jsx # Router + layout
│ ├── store.js # Signals (global state)
│ ├── components/
│ │ ├── Header.jsx
│ │ ├── TaskCard.jsx
│ │ ├── TaskForm.jsx
│ │ └── StatusBadge.jsx
│ └── pages/
│ ├── Dashboard.jsx
│ ├── TaskList.jsx
│ ├── NewTask.jsx
│ └── TaskDetail.jsx
├── index.html
├── package.json
└── vite.config.js
Global State with Signals
// src/store.js
import { signal, computed } from '@preact/signals';
export const tasks = signal([
{ id: 1, title: 'Set up CI/CD', status: 'done', priority: 'high' },
{ id: 2, title: 'Write unit tests', status: 'in-progress', priority: 'medium' },
{ id: 3, title: 'Code review PR #42', status: 'todo', priority: 'low' }
]);
export const filter = signal('all');
export const searchQuery = signal('');
export const filteredTasks = computed(() => {
let result = tasks.value;
if (filter.value !== 'all') {
result = result.filter(t => t.status === filter.value);
}
if (searchQuery.value) {
const q = searchQuery.value.toLowerCase();
result = result.filter(t => t.title.toLowerCase().includes(q));
}
return result;
});
export const stats = computed(() => {
const all = tasks.value;
return {
total: all.length,
todo: all.filter(t => t.status === 'todo').length,
inProgress: all.filter(t => t.status === 'in-progress').length,
done: all.filter(t => t.status === 'done').length
};
});
export function addTask(task) {
tasks.value = [...tasks.value, { ...task, id: Date.now() }];
}
export function updateTask(id, updates) {
tasks.value = tasks.value.map(t =>
t.id === id ? { ...t, ...updates } : t
);
}
export function deleteTask(id) {
tasks.value = tasks.value.filter(t => t.id !== id);
}
Output: The store module exports signals and actions. Any component can import and use them directly.
App Shell with Routing
// src/App.jsx
import { Router, Link } from 'preact-router';
import { Header } from './components/Header';
import Dashboard from './pages/Dashboard';
import TaskList from './pages/TaskList';
import NewTask from './pages/NewTask';
import TaskDetail from './pages/TaskDetail';
export function App() {
return (
<div>
<Header />
<main style={{ padding: 16, maxWidth: 800, margin: '0 auto' }}>
<Router>
<Dashboard path="/" />
<TaskList path="/tasks" />
<NewTask path="/tasks/new" />
<TaskDetail path="/tasks/:id" />
<div default style={{ textAlign: 'center', padding: 40 }}>
<h2>Page not found</h2>
<Link href="/">Go home</Link>
</div>
</Router>
</main>
</div>
);
}
Dashboard Component
// src/pages/Dashboard.jsx
import { stats } from '../store';
export default function Dashboard() {
const s = stats.value;
return (
<div>
<h2>Task Dashboard</h2>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16 }}>
<StatBox label="Total" value={s.total} color="#673ab8" />
<StatBox label="To Do" value={s.todo} color="#ff9800" />
<StatBox label="In Progress" value={s.inProgress} color="#2196f3" />
<StatBox label="Done" value={s.done} color="#4caf50" />
</div>
</div>
);
}
function StatBox({ label, value, color }) {
return (
<div style={{
background: color, color: '#fff', padding: 20, borderRadius: 8, textAlign: 'center'
}}>
<div style={{ fontSize: 32, fontWeight: 'bold' }}>{value}</div>
<div>{label}</div>
</div>
);
}
Output: Four colored cards display total, todo, in-progress, and done counts. The values update reactively as tasks change.
Task List with Filtering
// src/pages/TaskList.jsx
import { filter, searchQuery, filteredTasks, deleteTask } from '../store';
import { TaskCard } from '../components/TaskCard';
export default function TaskList() {
const items = filteredTasks.value;
return (
<div>
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
<input type="text" placeholder="Search tasks..."
onInput={(e) => searchQuery.value = e.target.value}
style={{ flex: 1, padding: 8 }} />
<select onChange={(e) => filter.value = e.target.value} value={filter.value}>
<option value="all">All</option>
<option value="todo">To Do</option>
<option value="in-progress">In Progress</option>
<option value="done">Done</option>
</select>
</div>
{items.length === 0 && <p>No tasks found.</p>}
{items.map(task => (
<TaskCard key={task.id} task={task} onDelete={deleteTask} />
))}
</div>
);
}
Task Form for Create and Edit
// src/components/TaskForm.jsx
import { useState, useEffect } from 'preact/hooks';
export function TaskForm({ initial, onSubmit, submitLabel = 'Save' }) {
const [form, setForm] = useState(initial || {
title: '', description: '', priority: 'medium', status: 'todo'
});
useEffect(() => {
if (initial) setForm(initial);
}, [initial]);
const handleSubmit = (e) => {
e.preventDefault();
if (!form.title.trim()) return;
onSubmit(form);
if (!initial) {
setForm({ title: '', description: '', priority: 'medium', status: 'todo' });
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>Title *</label>
<input type="text" value={form.title}
onInput={e => setForm(p => ({ ...p, title: e.target.value }))} />
</div>
<div>
<label>Description</label>
<textarea value={form.description}
onInput={e => setForm(p => ({ ...p, description: e.target.value }))} />
</div>
<div>
<label>Priority</label>
<select value={form.priority}
onChange={e => setForm(p => ({ ...p, priority: e.target.value }))}>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
<button type="submit">{submitLabel}</button>
</form>
);
}
Running the Project
npm run dev
Output: The task manager runs at http://localhost:5173. Users can view dashboard stats, filter and search tasks, create new tasks, and edit existing ones. All state is managed with signals for efficient updates.
Build and Optimize
npm run build
Output: The production build is under 15kB. The application loads instantly and runs smoothly on slow networks.
Mini Project Checklist
- Dashboard with reactive statistics
- Task list with search and filter
- Create and edit forms
- Routing between pages
- Signal-based global state
- Production build under 20kB
Extend the project by adding: due dates, user assignment, comments, file attachments, or dark mode theme.
FAQ
Congratulations
You've completed the Preact course. You now have a solid foundation in Preact development, from installation and components to signals, routing, forms, testing, and optimization. Continue building Preact applications and refer back to individual lessons when you need a refresher.
Return to the Preact course home to review any lesson or explore other web frameworks.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro