Перейти к содержимому

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 — Бизнес-логика
prisma/schema.prisma
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
}
src/routes/todos/index.tsx
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' }],
});
});
// Создание задачи
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 },
});
});
src/components/todo-item.tsx
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>
);
});