Back to blog

Giai đoạn 2: Phát triển Frontend với TypeScript

typescriptreactnextjsfrontendweb-development
Giai đoạn 2: Phát triển Frontend với TypeScript

Chào mừng đến với Giai đoạn 2

Giờ bạn đã nắm vững nền tảng TypeScript trong Giai đoạn 1, đã đến lúc áp dụng những kỹ năng đó để xây dựng ứng dụng frontend hiện đại. Trong giai đoạn này, bạn sẽ học cách kết hợp TypeScript với React 19 và Next.js 15 để tạo ra giao diện người dùng nhanh và type-safe.

Phát triển frontend với TypeScript là nơi hệ thống kiểu thực sự tỏa sáng. Bạn sẽ phát hiện lỗi prop lúc biên dịch, có autocompletion cho các API component, và xây dựng UI dễ refactor và bảo trì hơn.

Thời gian cam kết: 3 tuần, 1-2 giờ mỗi ngày Điều kiện tiên quyết: Giai đoạn 1: Nền tảng TypeScript

Bạn sẽ học được gì

Đến cuối Giai đoạn 2, bạn sẽ có thể:

✅ Xây dựng React component type-safe với prop typing đúng cách
✅ Sử dụng React hooks với hỗ trợ TypeScript đầy đủ
✅ Tạo custom hooks với generic type
✅ Làm việc với Next.js 15 App Router và Server Components
✅ Triển khai Server Actions với tính an toàn kiểu
✅ Quản lý state với Zustand và TanStack Query
✅ Xử lý form với React Hook Form + Zod validation
✅ Styling ứng dụng với Tailwind CSS và Shadcn/ui
✅ Cấu hình build tools (Vite, Turbopack)

Thiết lập dự án React + TypeScript

Lựa chọn 1: Next.js 15 (Khuyến nghị)

npx create-next-app@latest my-app --typescript --tailwind --eslint --app --src-dir
cd my-app
npm run dev

Điều này mang lại cho bạn:

  • TypeScript được cấu hình sẵn
  • App Router với Server Components
  • Tailwind CSS sẵn sàng
  • ESLint với rules của Next.js
  • Turbopack cho development build nhanh

Lựa chọn 2: Vite + React

npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev

Vite cung cấp setup nhanh, nhẹ cho ứng dụng React client-side.

Cấu trúc dự án (Next.js 15)

src/
├── app/                    # App Router pages
│   ├── layout.tsx          # Root layout
│   ├── page.tsx            # Trang chủ
│   └── blog/
│       ├── page.tsx        # Danh sách bài viết
│       └── [slug]/
│           └── page.tsx    # Bài viết động
├── components/             # Component tái sử dụng
│   ├── ui/                 # UI components cơ bản
│   ├── Header.tsx
│   └── Footer.tsx
├── lib/                    # Hàm tiện ích
│   ├── utils.ts
│   └── api.ts
├── hooks/                  # Custom React hooks
│   └── useDebounce.ts
└── types/                  # Định nghĩa kiểu dùng chung
    └── index.ts

React Components với TypeScript

Function Components và Props

// Định nghĩa props với interface
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: "primary" | "secondary" | "danger";
  disabled?: boolean;
  size?: "sm" | "md" | "lg";
}
 
function Button({ label, onClick, variant = "primary", disabled = false, size = "md" }: ButtonProps) {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      className={`btn btn-${variant} btn-${size}`}
    >
      {label}
    </button>
  );
}
 
// Sử dụng - TypeScript kiểm tra tất cả props
<Button label="Gửi" onClick={() => console.log("clicked")} />
<Button label="Xóa" onClick={handleDelete} variant="danger" />
// <Button label="Test" /> // Lỗi: onClick là bắt buộc

Children Props

// Sử dụng React.ReactNode cho children linh hoạt
interface CardProps {
  title: string;
  children: React.ReactNode;
  className?: string;
}
 
function Card({ title, children, className }: CardProps) {
  return (
    <div className={`card ${className ?? ""}`}>
      <h2>{title}</h2>
      <div>{children}</div>
    </div>
  );
}
 
// Sử dụng React.PropsWithChildren như một cách viết tắt
type PanelProps = React.PropsWithChildren<{
  title: string;
}>;
 
function Panel({ title, children }: PanelProps) {
  return (
    <section>
      <h3>{title}</h3>
      {children}
    </section>
  );
}

Generic Components

// Component danh sách generic
interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
  keyExtractor: (item: T) => string;
}
 
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={keyExtractor(item)}>{renderItem(item, index)}</li>
      ))}
    </ul>
  );
}
 
// Sử dụng - TypeScript suy luận T từ items
interface User {
  id: string;
  name: string;
  email: string;
}
 
const users: User[] = [
  { id: "1", name: "Alice", email: "[email protected]" },
  { id: "2", name: "Bob", email: "[email protected]" },
];
 
<List
  items={users}
  keyExtractor={(user) => user.id}
  renderItem={(user) => <span>{user.name} ({user.email})</span>}
/>

Event Handlers

// Các loại event handler phổ biến
function Form() {
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  };
 
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    // Xử lý gửi form
  };
 
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    console.log("Button clicked tại:", e.clientX, e.clientY);
  };
 
  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === "Enter") {
      // Xử lý phím Enter
    }
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} onKeyDown={handleKeyDown} />
      <button onClick={handleClick}>Gửi</button>
    </form>
  );
}

React Hooks với TypeScript

useState

// TypeScript suy luận kiểu từ giá trị ban đầu
const [count, setCount] = useState(0);          // number
const [name, setName] = useState("Alice");      // string
const [isOpen, setIsOpen] = useState(false);    // boolean
 
// Kiểu tường minh cho state phức tạp
interface User {
  id: string;
  name: string;
  email: string;
}
 
const [user, setUser] = useState<User | null>(null);
 
// Cập nhật với tính an toàn kiểu
setUser({ id: "1", name: "Alice", email: "[email protected]" });
// setUser({ id: "1", name: "Alice" }); // Lỗi: thiếu email

useRef

// Ref phần tử DOM
const inputRef = useRef<HTMLInputElement>(null);
 
function focusInput() {
  inputRef.current?.focus(); // Truy cập an toàn với optional chaining
}
 
return <input ref={inputRef} />;
 
// Ref có thể thay đổi (lưu giá trị, không phải phần tử DOM)
const renderCount = useRef<number>(0);
renderCount.current += 1; // Không cần optional chaining

useReducer

// Định nghĩa kiểu state và action
interface TodoState {
  todos: { id: string; text: string; done: boolean }[];
  filter: "all" | "active" | "completed";
}
 
type TodoAction =
  | { type: "ADD_TODO"; text: string }
  | { type: "TOGGLE_TODO"; id: string }
  | { type: "DELETE_TODO"; id: string }
  | { type: "SET_FILTER"; filter: TodoState["filter"] };
 
function todoReducer(state: TodoState, action: TodoAction): TodoState {
  switch (action.type) {
    case "ADD_TODO":
      return {
        ...state,
        todos: [...state.todos, { id: crypto.randomUUID(), text: action.text, done: false }],
      };
    case "TOGGLE_TODO":
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.id ? { ...todo, done: !todo.done } : todo
        ),
      };
    case "DELETE_TODO":
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id !== action.id),
      };
    case "SET_FILTER":
      return { ...state, filter: action.filter };
    default:
      return state;
  }
}
 
// Sử dụng trong component
const [state, dispatch] = useReducer(todoReducer, {
  todos: [],
  filter: "all",
});
 
dispatch({ type: "ADD_TODO", text: "Học TypeScript" });
dispatch({ type: "TOGGLE_TODO", id: "abc-123" });
// dispatch({ type: "ADD_TODO" }); // Lỗi: thiếu thuộc tính text

useContext với TypeScript

// Định nghĩa kiểu context
interface ThemeContextType {
  theme: "light" | "dark";
  toggleTheme: () => void;
}
 
// Tạo context với giá trị mặc định
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
 
// Custom hook để truy cập context an toàn
function useTheme(): ThemeContextType {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error("useTheme phải được sử dụng trong ThemeProvider");
  }
  return context;
}
 
// Component Provider
function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<"light" | "dark">("light");
 
  const toggleTheme = () => {
    setTheme((prev) => (prev === "light" ? "dark" : "light"));
  };
 
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}
 
// Sử dụng
function Header() {
  const { theme, toggleTheme } = useTheme(); // Được typed đầy đủ
  return <button onClick={toggleTheme}>Hiện tại: {theme}</button>;
}

Custom Hooks

Hook lấy dữ liệu Generic

interface UseFetchResult<T> {
  data: T | null;
  error: string | null;
  isLoading: boolean;
}
 
function useFetch<T>(url: string): UseFetchResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(true);
 
  useEffect(() => {
    const controller = new AbortController();
 
    async function fetchData() {
      try {
        setIsLoading(true);
        const response = await fetch(url, { signal: controller.signal });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const json = await response.json();
        setData(json as T);
      } catch (err) {
        if (err instanceof Error && err.name !== "AbortError") {
          setError(err.message);
        }
      } finally {
        setIsLoading(false);
      }
    }
 
    fetchData();
    return () => controller.abort();
  }, [url]);
 
  return { data, error, isLoading };
}
 
// Sử dụng - T được suy luận hoặc chỉ định
interface Post {
  id: number;
  title: string;
  body: string;
}
 
function BlogPosts() {
  const { data: posts, error, isLoading } = useFetch<Post[]>("/api/posts");
 
  if (isLoading) return <p>Đang tải...</p>;
  if (error) return <p>Lỗi: {error}</p>;
 
  return (
    <ul>
      {posts?.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

Hook Debounce

function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);
 
  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);
 
  return debouncedValue;
}
 
// Sử dụng
function SearchBar() {
  const [query, setQuery] = useState("");
  const debouncedQuery = useDebounce(query, 300);
 
  useEffect(() => {
    if (debouncedQuery) {
      // Lấy kết quả tìm kiếm
      console.log("Đang tìm kiếm:", debouncedQuery);
    }
  }, [debouncedQuery]);
 
  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

Next.js 15 với TypeScript

Server Components (Mặc định)

Trong Next.js 15, tất cả component trong thư mục app/ là Server Components theo mặc định. Chúng chạy trên server và có thể truy cập trực tiếp database, file, và các tài nguyên server-side khác.

// app/blog/page.tsx - Server Component (không cần "use client")
import { getAllPosts } from "@/lib/posts";
 
interface Post {
  slug: string;
  title: string;
  description: string;
  date: string;
}
 
export default async function BlogPage() {
  const posts: Post[] = await getAllPosts();
 
  return (
    <main>
      <h1>Blog</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.slug}>
            <a href={`/blog/${post.slug}`}>
              <h2>{post.title}</h2>
              <p>{post.description}</p>
              <time>{post.date}</time>
            </a>
          </li>
        ))}
      </ul>
    </main>
  );
}

Client Components

Thêm "use client" ở đầu file cần browser API, event handler, hoặc React state.

"use client";
 
import { useState } from "react";
 
interface CounterProps {
  initialCount?: number;
  step?: number;
}
 
export default function Counter({ initialCount = 0, step = 1 }: CounterProps) {
  const [count, setCount] = useState(initialCount);
 
  return (
    <div>
      <p>Đếm: {count}</p>
      <button onClick={() => setCount((c) => c + step)}>+{step}</button>
      <button onClick={() => setCount((c) => c - step)}>-{step}</button>
    </div>
  );
}

Dynamic Routes với TypeScript

// app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
import { getPostBySlug } from "@/lib/posts";
 
interface PageProps {
  params: Promise<{ slug: string }>;
}
 
export default async function BlogPostPage({ params }: PageProps) {
  const { slug } = await params;
  const post = await getPostBySlug(slug);
 
  if (!post) {
    notFound();
  }
 
  return (
    <article>
      <h1>{post.title}</h1>
      <time>{post.date}</time>
      <div>{post.content}</div>
    </article>
  );
}
 
// Tạo metadata cho SEO
export async function generateMetadata({ params }: PageProps) {
  const { slug } = await params;
  const post = await getPostBySlug(slug);
 
  if (!post) return { title: "Không tìm thấy" };
 
  return {
    title: post.title,
    description: post.description,
    openGraph: {
      title: post.title,
      description: post.description,
      images: [post.image],
    },
  };
}

Server Actions

Server Actions cho phép bạn chạy code server-side từ client component với tính an toàn kiểu đầy đủ.

// app/actions.ts
"use server";
 
import { z } from "zod";
 
const ContactSchema = z.object({
  name: z.string().min(2, "Tên phải có ít nhất 2 ký tự"),
  email: z.string().email("Địa chỉ email không hợp lệ"),
  message: z.string().min(10, "Tin nhắn phải có ít nhất 10 ký tự"),
});
 
type ContactFormState = {
  success: boolean;
  errors?: Record<string, string[]>;
  message?: string;
};
 
export async function submitContact(
  prevState: ContactFormState,
  formData: FormData
): Promise<ContactFormState> {
  const raw = {
    name: formData.get("name"),
    email: formData.get("email"),
    message: formData.get("message"),
  };
 
  const result = ContactSchema.safeParse(raw);
 
  if (!result.success) {
    return {
      success: false,
      errors: result.error.flatten().fieldErrors,
    };
  }
 
  // Lưu vào database, gửi email, v.v.
  console.log("Liên hệ đã gửi:", result.data);
 
  return { success: true, message: "Tin nhắn đã gửi thành công!" };
}
// app/contact/page.tsx
"use client";
 
import { useActionState } from "react";
import { submitContact } from "@/app/actions";
 
export default function ContactPage() {
  const [state, formAction, isPending] = useActionState(submitContact, {
    success: false,
  });
 
  return (
    <form action={formAction}>
      <div>
        <label htmlFor="name">Tên</label>
        <input id="name" name="name" required />
        {state.errors?.name && <p className="text-red-500">{state.errors.name[0]}</p>}
      </div>
 
      <div>
        <label htmlFor="email">Email</label>
        <input id="email" name="email" type="email" required />
        {state.errors?.email && <p className="text-red-500">{state.errors.email[0]}</p>}
      </div>
 
      <div>
        <label htmlFor="message">Tin nhắn</label>
        <textarea id="message" name="message" required />
        {state.errors?.message && <p className="text-red-500">{state.errors.message[0]}</p>}
      </div>
 
      <button type="submit" disabled={isPending}>
        {isPending ? "Đang gửi..." : "Gửi tin nhắn"}
      </button>
 
      {state.success && <p className="text-green-500">{state.message}</p>}
    </form>
  );
}

Quản lý State

Zustand (Store nhẹ)

Zustand cung cấp store đơn giản, type-safe không cần boilerplate.

import { create } from "zustand";
 
// Định nghĩa kiểu store
interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}
 
interface CartStore {
  items: CartItem[];
  addItem: (item: Omit<CartItem, "quantity">) => void;
  removeItem: (id: string) => void;
  updateQuantity: (id: string, quantity: number) => void;
  clearCart: () => void;
  total: () => number;
}
 
const useCartStore = create<CartStore>((set, get) => ({
  items: [],
 
  addItem: (item) =>
    set((state) => {
      const existing = state.items.find((i) => i.id === item.id);
      if (existing) {
        return {
          items: state.items.map((i) =>
            i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
          ),
        };
      }
      return { items: [...state.items, { ...item, quantity: 1 }] };
    }),
 
  removeItem: (id) =>
    set((state) => ({
      items: state.items.filter((i) => i.id !== id),
    })),
 
  updateQuantity: (id, quantity) =>
    set((state) => ({
      items: state.items.map((i) => (i.id === id ? { ...i, quantity } : i)),
    })),
 
  clearCart: () => set({ items: [] }),
 
  total: () => get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),
}));
 
// Sử dụng trong component
function CartSummary() {
  const items = useCartStore((state) => state.items);
  const total = useCartStore((state) => state.total());
  const clearCart = useCartStore((state) => state.clearCart);
 
  return (
    <div>
      <p>{items.length} sản phẩm trong giỏ hàng</p>
      <p>Tổng: ${total.toFixed(2)}</p>
      <button onClick={clearCart}>Xóa giỏ hàng</button>
    </div>
  );
}

TanStack Query (Lấy dữ liệu)

TanStack Query (React Query) quản lý server state với caching, refetching, và tính an toàn kiểu.

import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
 
interface Post {
  id: number;
  title: string;
  body: string;
  userId: number;
}
 
// Hàm query được typed
async function fetchPosts(): Promise<Post[]> {
  const response = await fetch("/api/posts");
  if (!response.ok) throw new Error("Không thể lấy bài viết");
  return response.json();
}
 
async function createPost(data: Omit<Post, "id">): Promise<Post> {
  const response = await fetch("/api/posts", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data),
  });
  if (!response.ok) throw new Error("Không thể tạo bài viết");
  return response.json();
}
 
// Query hook - data được typed là Post[] | undefined
function PostList() {
  const { data: posts, isLoading, error } = useQuery({
    queryKey: ["posts"],
    queryFn: fetchPosts,
  });
 
  const queryClient = useQueryClient();
 
  const mutation = useMutation({
    mutationFn: createPost,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["posts"] });
    },
  });
 
  if (isLoading) return <p>Đang tải...</p>;
  if (error) return <p>Lỗi: {error.message}</p>;
 
  return (
    <div>
      <button
        onClick={() =>
          mutation.mutate({ title: "Bài viết mới", body: "Nội dung", userId: 1 })
        }
      >
        Thêm bài viết
      </button>
      <ul>
        {posts?.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}

Xử lý Form

React Hook Form + Zod

Sự kết hợp React Hook Form và Zod cung cấp tính an toàn kiểu tuyệt vời cho form.

"use client";
 
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
 
// Định nghĩa schema với Zod
const signUpSchema = z
  .object({
    username: z
      .string()
      .min(3, "Tên người dùng phải có ít nhất 3 ký tự")
      .max(20, "Tên người dùng tối đa 20 ký tự"),
    email: z.string().email("Địa chỉ email không hợp lệ"),
    password: z
      .string()
      .min(8, "Mật khẩu phải có ít nhất 8 ký tự")
      .regex(/[A-Z]/, "Phải chứa một chữ hoa")
      .regex(/[0-9]/, "Phải chứa một số"),
    confirmPassword: z.string(),
  })
  .refine((data) => data.password === data.confirmPassword, {
    message: "Mật khẩu không khớp",
    path: ["confirmPassword"],
  });
 
// Suy luận kiểu TypeScript từ Zod schema
type SignUpFormData = z.infer<typeof signUpSchema>;
 
export default function SignUpForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<SignUpFormData>({
    resolver: zodResolver(signUpSchema),
  });
 
  const onSubmit = async (data: SignUpFormData) => {
    // data được typed đầy đủ: { username, email, password, confirmPassword }
    console.log("Form đã gửi:", data);
  };
 
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <input {...register("username")} placeholder="Tên người dùng" />
        {errors.username && <p>{errors.username.message}</p>}
      </div>
 
      <div>
        <input {...register("email")} type="email" placeholder="Email" />
        {errors.email && <p>{errors.email.message}</p>}
      </div>
 
      <div>
        <input {...register("password")} type="password" placeholder="Mật khẩu" />
        {errors.password && <p>{errors.password.message}</p>}
      </div>
 
      <div>
        <input {...register("confirmPassword")} type="password" placeholder="Xác nhận mật khẩu" />
        {errors.confirmPassword && <p>{errors.confirmPassword.message}</p>}
      </div>
 
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Đang đăng ký..." : "Đăng ký"}
      </button>
    </form>
  );
}

Styling với TypeScript

Tailwind CSS

Tailwind CSS hoạt động hoàn hảo với TypeScript. Sử dụng hàm tiện ích cn() cho class có điều kiện:

import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
 
// Hàm tiện ích để merge class Tailwind
export function cn(...inputs: ClassValue[]): string {
  return twMerge(clsx(inputs));
}
 
// Sử dụng trong component
interface BadgeProps {
  label: string;
  variant: "success" | "warning" | "error" | "info";
}
 
function Badge({ label, variant }: BadgeProps) {
  return (
    <span
      className={cn(
        "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
        {
          "bg-green-100 text-green-800": variant === "success",
          "bg-yellow-100 text-yellow-800": variant === "warning",
          "bg-red-100 text-red-800": variant === "error",
          "bg-blue-100 text-blue-800": variant === "info",
        }
      )}
    >
      {label}
    </span>
  );
}

Shadcn/ui Components

Shadcn/ui cung cấp các component copy-paste với hỗ trợ TypeScript đầy đủ:

npx shadcn@latest init
npx shadcn@latest add button card input
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
 
// Tất cả component đều được typed đầy đủ
function Dashboard() {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Bảng điều khiển</CardTitle>
      </CardHeader>
      <CardContent className="space-y-4">
        <Input placeholder="Tìm kiếm..." type="search" />
        <div className="flex gap-2">
          <Button variant="default">Lưu</Button>
          <Button variant="outline">Hủy</Button>
          <Button variant="destructive">Xóa</Button>
        </div>
      </CardContent>
    </Card>
  );
}

Cấu hình Build Tools

Phát triển frontend hiện đại dựa vào các build tool như Vite và webpack để bundle, tối ưu và phục vụ code. Để hiểu sâu hơn về cách các công cụ này hoạt động, xem JavaScript Build Tools & Bundlers Explained.

Vite với TypeScript

// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";
 
export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
  server: {
    port: 3000,
    open: true,
  },
  build: {
    outDir: "dist",
    sourcemap: true,
  },
});

Cấu hình Next.js

// next.config.ts (Next.js 15 hỗ trợ cấu hình .ts)
import type { NextConfig } from "next";
 
const nextConfig: NextConfig = {
  reactStrictMode: true,
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "images.unsplash.com",
      },
    ],
  },
  experimental: {
    typedRoutes: true, // Routing type-safe
  },
};
 
export default nextConfig;

Cấu hình TypeScript cho React

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["DOM", "DOM.Iterable", "ES2022"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "strict": true,
    "noEmit": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "incremental": true,
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src", "next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

Các Pattern và Best Practices phổ biến

Discriminated Union cho UI States

type AsyncState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: string };
 
function UserProfile() {
  const [state, setState] = useState<AsyncState<User>>({ status: "idle" });
 
  switch (state.status) {
    case "idle":
      return <p>Nhấn để tải hồ sơ</p>;
    case "loading":
      return <p>Đang tải...</p>;
    case "success":
      return <p>Xin chào, {state.data.name}!</p>; // data được typed
    case "error":
      return <p>Lỗi: {state.error}</p>; // error được typed
  }
}

Forwarding Refs

import { forwardRef } from "react";
 
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label: string;
  error?: string;
}
 
const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ label, error, className, ...props }, ref) => {
    return (
      <div>
        <label>{label}</label>
        <input ref={ref} className={className} {...props} />
        {error && <p className="text-red-500 text-sm">{error}</p>}
      </div>
    );
  }
);
 
Input.displayName = "Input";

Polymorphic Components

// Component render như các phần tử HTML khác nhau
type AsProp<C extends React.ElementType> = {
  as?: C;
};
 
type PolymorphicProps<C extends React.ElementType, Props = object> = AsProp<C> &
  Props &
  Omit<React.ComponentPropsWithoutRef<C>, keyof AsProp<C> | keyof Props>;
 
interface TextOwnProps {
  size?: "sm" | "md" | "lg";
  weight?: "normal" | "bold";
}
 
function Text<C extends React.ElementType = "p">({
  as,
  size = "md",
  weight = "normal",
  children,
  ...props
}: PolymorphicProps<C, TextOwnProps>) {
  const Component = as || "p";
  return (
    <Component className={`text-${size} font-${weight}`} {...props}>
      {children}
    </Component>
  );
}
 
// Sử dụng
<Text>Đoạn văn mặc định</Text>
<Text as="h1" size="lg" weight="bold">Tiêu đề</Text>
<Text as="span" size="sm">Văn bản inline</Text>

Tóm tắt và Điểm chính

Bạn đã hoàn thành Giai đoạn 2 của lộ trình TypeScript. Đây là những gì bạn đã học:

✅ React Components: Typing props, children, generic components, event handlers
✅ Hooks: useState, useRef, useReducer, useContext với tính an toàn kiểu đầy đủ
✅ Custom Hooks: Generic hooks, lấy dữ liệu, debounce patterns
✅ Next.js 15: Server Components, Client Components, dynamic routes, Server Actions
✅ Quản lý State: Zustand stores, TanStack Query cho server state
✅ Forms: React Hook Form + Zod cho form validated, type-safe
✅ Styling: Tiện ích Tailwind CSS, component Shadcn/ui
✅ Build Tools: Cấu hình Vite và Next.js
✅ Patterns: Discriminated unions, forwarding refs, polymorphic components

Tiếp theo là gì?

Với kỹ năng frontend đã có, Giai đoạn 3 sẽ đưa bạn đến backend nơi bạn sẽ xây dựng API type-safe với Node.js, Express, và Prisma.

🎯 Tiếp tục Giai đoạn 3: Phát triển Backend →

Tài nguyên bổ sung

Để tìm hiểu sâu hơn về React + TypeScript patterns:

📘 Deep Dive: React với TypeScript Best Practices →


Trước đó: Giai đoạn 1: Nền tảng TypeScript → Tiếp theo: Giai đoạn 3: Phát triển Backend →

Happy coding! 🚀

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.