21. Реальный проект: Todo App
В этом уроке мы рассмотрим полноценное Todo приложение, которое демонстрирует все ключевые возможности Qwik: сигналы для состояния, routeAction$ для CRUD операций, валидацию форм, оптимистичный UI и серверный рендеринг.
Архитектура приложения
Заголовок раздела «Архитектура приложения»src/├── routes/│ ├── layout.tsx — Общий лейаут│ └── todos/│ ├── index.tsx — Список задач (routeLoader$)│ └── [id]/│ └── index.tsx — Детальная страница задачи├── components/│ ├── todo-item.tsx — Компонент задачи│ ├── todo-form.tsx — Форма создания│ └── todo-filter.tsx — Фильтрация задач└── lib/ ├── db.ts — Prisma клиент └── todos.ts — Бизнес-логикаМодель данных
Заголовок раздела «Модель данных»model Todo { id String @id @default(cuid()) title String description String? completed Boolean @default(false) priority Priority @default(MEDIUM) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt}
enum Priority { LOW MEDIUM HIGH}Загрузка и фильтрация данных
Заголовок раздела «Загрузка и фильтрация данных»export const useTodos = routeLoader$(async ({ url }) => { const filter = url.searchParams.get('filter') ?? 'all'; const search = url.searchParams.get('q') ?? '';
return await prisma.todo.findMany({ where: { ...(filter === 'active' && { completed: false }), ...(filter === 'done' && { completed: true }), ...(search && { title: { contains: search, mode: 'insensitive' } }), }, orderBy: [{ completed: 'asc' }, { createdAt: 'desc' }], });});CRUD действия
Заголовок раздела «CRUD действия»// Создание задачиexport const useCreateTodo = routeAction$( async (data) => { return await prisma.todo.create({ data }); }, zod$({ title: z.string().min(1, 'Введите название').max(200), description: z.string().optional(), priority: z.enum(['LOW', 'MEDIUM', 'HIGH']).default('MEDIUM'), }));
// Переключение статусаexport const useToggleTodo = routeAction$(async (data) => { return await prisma.todo.update({ where: { id: data.id as string }, data: { completed: data.completed === 'true' }, });});
// Удаление задачиexport const useDeleteTodo = routeAction$(async (data) => { return await prisma.todo.delete({ where: { id: data.id as string }, });});Компонент задачи с оптимистичным UI
Заголовок раздела «Компонент задачи с оптимистичным UI»export const TodoItem = component$<{ todo: Todo; onToggle$: PropFunction<(id: string) => void>; onDelete$: PropFunction<(id: string) => void>;}>((props) => { const isDeleting = useSignal(false); const isToggling = useSignal(false);
// Оптимистичный статус const optimisticCompleted = isToggling.value ? !props.todo.completed // Сразу показываем результат : props.todo.completed;
return ( <div class={{ 'todo-item': true, 'todo-item--done': optimisticCompleted }}> <button onClick$={async () => { isToggling.value = true; await props.onToggle$(props.todo.id); isToggling.value = false; }} class="todo-toggle" > {optimisticCompleted ? '✅' : '⬜'} </button>
<span class="todo-title">{props.todo.title}</span>
<button onClick$={async () => { isDeleting.value = true; await props.onDelete$(props.todo.id); }} disabled={isDeleting.value} > {isDeleting.value ? '⏳' : '🗑️'} </button> </div> );});