Эцсийн төсөл
Та React курсын 44 хичээлийг дуусгасан — component, state, hooks, TypeScript, тест, pattern бүгдийг сурсан. Одоо эдгээрийг нэгтгэж бодит апп бүтээх цаг боллоо. Энэ төслийн зорилго нь ганцхан зүйл: сурсан мэдлэгээ гараараа хийж батлах.
Юу бүтээх вэ?
Хувийн номын сан — хэрэглэгч ном нэмж, уншсан эсэхийг тэмдэглэж, дуртай номоо хадгалдаг апп. Жижиг боловч бодит хэрэглээний бүх шаардлагыг хангасан суурь төсөл.
Аппын үндсэн чадварууд:
✓ Ном нэмэх, засах, устгах
✓ "Уншсан" / "Унших гэж байна" / "Уншиж байна" гэсэн төлвүүд
✓ Нэр, зохиолч, жанраар шүүх
✓ Local storage-д хадгалах — хуудас дахин ачаалсан ч хэвээр байх
✓ TypeScript — бүх өгөгдлийн бүтэц тодорхой
✓ Vitest-р нэг custom hook-ын тест
Өгөгдлийн бүтэц
Эхлээд төрлүүдийг тодорхойлно — TypeScript-н хамгийн чухал алхам:
// src/types/book.ts
export type ReadStatus = "хүлээгдэж_буй" | "уншиж_байна" | "уншсан";
export interface Book {
id: string;
title: string;
author: string;
genre: string;
status: ReadStatus;
rating: number | null; // 1–5 од, уншаагүй бол null
addedAt: number; // timestamp
finishedAt: number | null;
}
export interface BookFilter {
search: string;
genre: string | null;
status: ReadStatus | null;
}
useBooks custom hook
Апп-н бүх логик нэг custom hook-д:
// src/hooks/useBooks.ts
import { useState, useEffect, useCallback, useMemo } from "react";
import type { Book, BookFilter, ReadStatus } from "../types/book";
const STORAGE_KEY = "my-library-books";
function generateId(): string {
return `book-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
}
export function useBooks() {
const [books, setBooks] = useState<Book[]>(() => {
try {
const saved = localStorage.getItem(STORAGE_KEY);
return saved ? (JSON.parse(saved) as Book[]) : [];
} catch {
return [];
}
});
const [filter, setFilter] = useState<BookFilter>({
search: "",
genre: null,
status: null,
});
// books өөрчлөгдөх бүрт localStorage-д хадгал
useEffect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(books));
}, [books]);
const addBook = useCallback(
(data: Omit<Book, "id" | "addedAt" | "finishedAt" | "rating">) => {
const newBook: Book = {
...data,
id: generateId(),
rating: null,
addedAt: Date.now(),
finishedAt: null,
};
setBooks((prev) => [newBook, ...prev]);
},
[],
);
const updateStatus = useCallback((id: string, status: ReadStatus) => {
setBooks((prev) =>
prev.map((b) =>
b.id === id
? {
...b,
status,
finishedAt: status === "уншсан" ? Date.now() : null,
}
: b,
),
);
}, []);
const rateBook = useCallback((id: string, rating: number) => {
setBooks((prev) => prev.map((b) => (b.id === id ? { ...b, rating } : b)));
}, []);
const deleteBook = useCallback((id: string) => {
setBooks((prev) => prev.filter((b) => b.id !== id));
}, []);
// Шүүлт болон хайлт — useMemo-р кэшлэнэ
const filteredBooks = useMemo(() => {
return books.filter((b) => {
const matchSearch =
filter.search === "" ||
b.title.toLowerCase().includes(filter.search.toLowerCase()) ||
b.author.toLowerCase().includes(filter.search.toLowerCase());
const matchGenre = filter.genre === null || b.genre === filter.genre;
const matchStatus = filter.status === null || b.status === filter.status;
return matchSearch && matchGenre && matchStatus;
});
}, [books, filter]);
const stats = useMemo(
() => ({
total: books.length,
read: books.filter((b) => b.status === "уншсан").length,
reading: books.filter((b) => b.status === "уншиж_байна").length,
pending: books.filter((b) => b.status === "хүлээгдэж_буй").length,
}),
[books],
);
return {
books: filteredBooks,
stats,
filter,
setFilter,
addBook,
updateStatus,
rateBook,
deleteBook,
};
}
Апп-н бүтэц
src/
├── types/
│ └── book.ts
├── hooks/
│ ├── useBooks.ts
│ └── useBooks.test.ts ← hook-н тест
├── components/
│ ├── BookCard/
│ │ ├── BookCard.tsx
│ │ └── BookCard.stories.tsx
│ ├── AddBookForm/
│ │ └── AddBookForm.tsx
│ ├── FilterBar/
│ │ └── FilterBar.tsx
│ └── StatsBar/
│ └── StatsBar.tsx
└── App.tsx
App.tsx нь useBooks hook дуудаж, бүх component-д тохирох props дамжуулна — Provider шаардлагагүй, учир нь state нэг хэсэгт л байна.
Hook-н тест
// src/hooks/useBooks.test.ts
import { renderHook, act } from "@testing-library/react";
import { describe, it, expect, beforeEach } from "vitest";
import { useBooks } from "./useBooks";
describe("useBooks hook", () => {
beforeEach(() => {
localStorage.clear();
});
it("эхэндээ хоосон байна", () => {
const { result } = renderHook(() => useBooks());
expect(result.current.books).toHaveLength(0);
expect(result.current.stats.total).toBe(0);
});
it("ном нэмэхэд жагсаалтад орно", () => {
const { result } = renderHook(() => useBooks());
act(() => {
result.current.addBook({
title: "Цэнхэр толь",
author: "Д.Нацагдорж",
genre: "Яруу найраг",
status: "хүлээгдэж_буй",
});
});
expect(result.current.books).toHaveLength(1);
expect(result.current.books[0].title).toBe("Цэнхэр толь");
expect(result.current.stats.total).toBe(1);
expect(result.current.stats.pending).toBe(1);
});
it("төлөв солиход stats шинэчлэгдэнэ", () => {
const { result } = renderHook(() => useBooks());
act(() => {
result.current.addBook({
title: "Тест ном",
author: "Зохиолч",
genre: "Роман",
status: "хүлээгдэж_буй",
});
});
const id = result.current.books[0].id;
act(() => {
result.current.updateStatus(id, "уншсан");
});
expect(result.current.stats.read).toBe(1);
expect(result.current.stats.pending).toBe(0);
expect(result.current.books[0].finishedAt).not.toBeNull();
});
});
Цааш хөгжүүлэх санаанууд
Суурь апп ажилласны дараа нэмж болох зүйлс:
□ react-router нэмж /library, /add, /book/:id маршрутууд
□ Supabase-тай холбож өгөгдлийг cloud-д хадгалах
□ Drag-and-drop Kanban самбар (Unread → Reading → Done)
□ Ном хайх Google Books API-тай холбох (үнэгүй)
□ Стройбük-д BookCard-н бүх story нэмэх
□ E2E тест Playwright-аар бичих
Энэ апп дуусвал GitHub-д байршуулаад README-д "React, TypeScript, Vitest, Storybook ашигласан" гэж бич — ажлын байранд CV-д тавихад тохиромжтой бодит төсөл болно.
Курс дуусгасанд баяр хүргэе!
45 хичээл дуусгасан — component-аас эхлэн TypeScript, тест, архитектурын pattern хүртэл React-н чухал бүх хэсгийг дамжлаа. Хамгийн чухал алхам дараагийнх: сурсан зүйлсээ бодит төсөлд хэрэглэх. Код бичих нь цорын ганц арга — ном уншиж, видео үзэж дэлгэрэнгүй мэдэх нь практиктай тэнцэхгүй. Цааш суралцах бол Next.js курсыг шалгаарай — React-н суурин дээр full-stack апп бүтээх дараагийн алхам тэнд байгаа.
Дараагийн хичээлд:
Энэ курс дуусгасан таны дараагийн алхам бол Next.js үндэс курс — React мэдлэгийн дээр сервер side rendering, file-based routing, Supabase-тай full-stack апп бүтээх ур чадвар нэмэгдэнэ.