Thư viện: npm install -g json-server
Chạy: json-server --watch db.json --port 3000
Câu lệnh xét quyền Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
Bảng Tra Cứu (Cheatsheet Prefix)
ex-lythuyet : (Câu 1) Gen đoạn comment lý thuyết về RecipeCard và React.memo.
ex-focus : (Câu 1) Gen ngay useRef + useEffect để focus.
Nhóm API Axios (Câu 2, 5, 7, 8):
ex-api-get : Viết hàm GET danh sách.
ex-api-get-id : Viết hàm GET chi tiết theo ID.
ex-api-post : Viết hàm POST thêm mới.
ex-api-delete : Viết hàm DELETE xóa theo ID.
ex-api-put : Viết hàm PUT cập nhật.
Nhóm Logic Component:
ex-usefetch-adv : (Câu 4) Gen custom hook dùng fetcher và dependency.
ex-filter : (Câu 6) Gen logic lọc recipe theo độ khó và từ khóa.
ex-reducer : (Câu 7) Gen Form Reducer xử lý thêm nguyên liệu (ingredients).
ex-submit : (Câu 7) Gen logic Validate form và AddRecipe.
ex-actions : (Câu 8) Gen useCallback xóa công thức và thả tim món ăn.
ex-login-logic : (Câu 9) Gen hàm Login (tự động điền tài khoản chef / 1234).
ex-protect : (Câu 10) Gen Component chặn các trang cần đăng nhập.
Danh sách câu hỏi 1-10
Câu 1
- RecipeCard: viết comment trả lời 2 ý:
- Liệt kê các props mà RecipeCard nhận.
- Giải thích vì sao bọc bằng React.memo lại cần thiết khi có nhiều card. Khi nào memo KHÔNG có tác dụng?
- SearchBox: dùng useRef + useEffect để focus input khi component mount.
Câu 2
- recipeApi: viết hàm GET danh sách công thức bằng axios.
- RecipeListPage: dùng custom hook useFetch để fetch danh sách công thức, sau khi có data thì đồng bộ vào Recoil recipesState bằng useEffect để các trang khác (Home, Favorites) cũng có dữ liệu.
Câu 3
- Sử dụng đúng các atom đã khai báo trong store/atoms.js (recipesState, userState, difficultyFilterState) thông qua useRecoilState / useRecoilValue / useSetRecoilState ở các trang liên quan.
Câu 4
- useFetch: hoàn thiện custom hook theo yêu cầu:
- Nhận vào fetcher (hàm async) và deps (mặc định []).
- State: data (mặc định null), loading (mặc định true), error (mặc định null).
- Khi mount (và khi deps thay đổi): set loading = true, gọi fetcher();
thành công -> setData, setLoading(false); thất bại -> setError, setLoading(false).
- Trả về { data, loading, error, refetch }, trong đó refetch dùng để gọi lại fetcher.
- ThemeContext: nâng cấp ThemeProvider:
- Khởi tạo theme ban đầu lấy từ localStorage (key 'theme'), nếu chưa có thì dùng 'light'.
- Mỗi khi theme thay đổi: lưu vào localStorage và cập nhật class trên document.body - 'light' -> xóa class 'dark' - 'dark' -> thêm class 'dark'
Lưu ý: giữ nguyên tên { theme, toggleTheme } trong value của Provider.
Câu 5
- recipeApi: viết hàm GET chi tiết công thức theo id.
- RecipeDetailPage: dùng useEffect gọi getRecipeById(id), có xử lý loading, khi id đổi thì fetch lại.
Câu 6
- RecipeListPage: lọc danh sách công thức hiển thị theo:
- difficulty ('all' | 'easy' | 'medium' | 'hard')
- keyword: tìm theo tên (title), không phân biệt hoa thường.
- HomePage: dùng useMemo để tính:
- total: tổng số công thức
- easy: số công thức độ khó 'easy'
- medium: số công thức độ khó 'medium'
- hard: số công thức độ khó 'hard'
- favoriteCount: số công thức có favorite === true
- avgCookTime: thời gian nấu trung bình (làm tròn nguyên), 0 nếu chưa có công thức nào.
- FavoritesPage: dùng useMemo lọc ra các công thức có favorite === true.
Câu 7
- recipeApi: viết hàm POST thêm công thức mới.
- AddRecipePage:
- Hoàn thiện reducer cho form thêm công thức.
State gồm: title, difficulty, cookTime, servings, ingredients (mảng chuỗi), description, error.
Action cần xử lý: - { type: 'SET_FIELD', field, value } - { type: 'ADD_INGREDIENT' } - { type: 'UPDATE_INGREDIENT', index, value } - { type: 'REMOVE_INGREDIENT', index } - { type: 'SET_ERROR', error } - { type: 'RESET' }
- handleSubmit phải:
- Validate: title không rỗng, cookTime > 0, servings > 0,
ingredients có ít nhất 1 dòng không rỗng (sau khi trim).
Sai -> dispatch SET_ERROR và dừng.
- Chuẩn hóa: lọc bỏ ingredient rỗng (trim() === '').
- Gọi addRecipe(payload), cập nhật recipesState (thêm vào danh sách).
- dispatch RESET, navigate('/recipes').
Câu 8
- recipeApi: viết hàm DELETE công thức theo id.
- recipeApi: viết hàm PUT cập nhật trạng thái yêu thích (favorite: boolean).
- RecipeListPage: viết handleDelete dùng useCallback:
- window.confirm trước khi xóa
- gọi deleteRecipe(id)
- cập nhật recipesState (loại bỏ công thức đã xóa).
- RecipeDetailPage: viết handleToggleFavorite:
- gọi toggleFavorite(id, newValue) (newValue ngược lại với recipe.favorite hiện tại)
- cập nhật state recipe (đảo favorite)
- cập nhật Recoil recipesState để danh sách ngoài kia đồng bộ.
Câu 9
- LoginPage:
- Dùng useLocalStorage để có biến savedUser.
- Viết handleLogin:
- Validate: username, password không rỗng.
- Đúng (chef / 1234) -> setUser({ username }), setSavedUser({ username }), navigate('/').
- Sai -> setError("Sai tài khoản hoặc mật khẩu").
- App.jsx:
- Lấy user từ Recoil để hiển thị tên + nút Logout trên navbar.
- Khi đã login, ẩn link Đăng nhập, hiện "Xin chào, {username}" + nút Logout.
Câu 10
- App.jsx: bọc /add và /favorites bằng ProtectedRoute.