Skip to content

Preact Mini Project — Build a Complete Preact Application

DodaTech Updated 2026-06-28 6 min read

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

How do I add authentication to this project?

: Add an AuthContext that stores the user session. Protect routes by checking the auth context and redirecting unauthenticated users.

Can I deploy this to production?

: Yes. Run npm run build and deploy the dist/ directory to any static host (Netlify, Vercel, Cloudflare Pages).

How do I add a backend API?

: Use fetch() in your signal actions or add a store action that calls an API and updates the signal with the response.

Can I persist task data?

: Add an effect that saves tasks to localStorage on every change: effect(() => localStorage.setItem('tasks', JSON.stringify(tasks.value))).

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