Remix.js: Xây dựng Web App “Chuẩn” Web Standards – Từ Data Loading đến Server Actions

Development tutorial - IT technology blog
Development tutorial - IT technology blog

Remix.js: Làn gió mới hay sự trở lại của các giá trị cốt lõi?

Nếu đã từng làm Single Page Application (SPA) với React, chắc hẳn bạn không lạ gì cảnh ngồi chờ cái loading spinner xoay tít mù mỗi khi chuyển trang. Dữ liệu thường được fetch ở phía client sau khi component mount. Cách làm này tạo ra những khoảng hẫng về trải nghiệm, nhất là khi người dùng truy cập bằng 4G “hết vạch” ở quán cafe hay trên tàu cao tốc.

Mình từng tốn hàng tuần trời chỉ để loay hoay với useEffect, chống race condition và cấu hình các thư viện state management cồng kềnh. Mọi thứ thay đổi khi mình thử Remix.js. Thay vì đẻ ra hàng tá khái niệm phức tạp, Remix chọn đi ngược dòng thời gian, tận dụng triệt để những tiêu chuẩn Web (Web Standards) vốn đã cực kỳ mạnh mẽ nhưng thường bị lãng quên.

Triết lý sống còn của Remix là Progressive Enhancement. Nói đơn giản: ứng dụng của bạn phải chạy được ngay cả khi JavaScript bị lỗi hoặc chưa kịp tải xong. Cách tiếp cận này không chỉ giúp trang web phản hồi tức thì mà còn là “liều thuốc bổ” cho SEO – yếu tố quyết định doanh thu mà anh em Dev đôi khi hay xem nhẹ.

Remix xóa sổ hoàn toàn tình trạng “Waterfall fetching” (các request chồng chéo lên nhau) bằng các loader chạy song song trên server. Bạn sẽ không còn phải lo lộ API key bí mật hay nhồi nhét logic xử lý dữ liệu nặng nề xuống trình duyệt của người dùng nữa.

Khởi tạo dự án: Nhanh, gọn và chuẩn chỉnh

Để bắt đầu, hãy đảm bảo máy bạn đã cài Node.js v18+. Chỉ cần một dòng lệnh duy nhất để thiết lập mọi thứ:

npx create-remix@latest my-remix-app

Trong lúc cài đặt, mình thường ưu tiên các lựa chọn sau để quá trình phát triển sau này “dễ thở” hơn:

  • Directory: Đặt tên dự án rõ ràng, ví dụ: remix-ecommerce-v1.
  • Install dependencies: Chọn Yes (dùng npm hoặc pnpm cho nhanh).
  • TypeScript: Luôn luôn chọn Yes. Khả năng tự động suy luận kiểu dữ liệu từ loader sang component của Remix cực kỳ bá đạo, giúp bạn tránh được những lỗi undefined ngớ ngẩn.
cd my-remix-app
npm run dev

Ứng dụng sẽ khởi chạy tại localhost:3000. Cấu trúc thư mục của Remix rất trực quan, hầu hết thời gian bạn sẽ làm việc trong thư mục app/ – nơi chứa các route và logic xử lý chính.

Bí kíp làm chủ Loader, Action và Progressive Enhancement

Đây chính là bộ ba tạo nên sự khác biệt cho Remix. Hãy cùng xem cách chúng ta xử lý một trang danh sách sản phẩm trong thực tế.

1. Data Loading với Loader (Server-side fetch)

Mỗi file route trong Remix đóng vai trò như một API endpoint nhỏ. Hàm loader chỉ chạy trên server, giúp bạn truy vấn trực tiếp vào Database mà không cần thông qua một tầng API trung gian nào khác.

// app/routes/products.tsx
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";

export const loader = async () => {
  // Lấy dữ liệu từ DB, ví dụ 10 sản phẩm mới nhất
  const products = [
    { id: 1, name: "Bàn phím cơ Akko", price: 1200000 },
    { id: 2, name: "Chuột Logitech G502", price: 850000 },
  ];
  return json({ products });
};

export default function ProductsPage() {
  const { products } = useLoaderData<typeof loader>();
  
  return (
    <div>
      <h1 className="text-2xl font-bold">Sản phẩm hot</h1>
      <ul>
        {products.map(p => (
          <li key={p.id}>{p.name} - {p.price.toLocaleString()} VNĐ</li>
        ))}
      </ul>
    </div>
  );
}

Kinh nghiệm xương máu là khi làm việc với các JSON lồng nhau nhiều tầng từ Shopify hay Contentful, dữ liệu rất dễ bị rối. Mình thường dùng JSON Formatter để làm sạch và soi cấu trúc dữ liệu trước khi định nghĩa Interface trong TypeScript – giúp tiết kiệm ít nhất 15 phút mỗi khi debug.

2. Thay đổi dữ liệu với Server Actions

Quên useState hay onSubmit dài dòng đi. Remix đưa chúng ta trở lại với thẻ <Form> thần thánh nhưng mạnh mẽ hơn nhiều. Khi nhấn nút gửi, hàm action trên server sẽ tiếp nhận và xử lý dữ liệu.

// app/routes/products/new.tsx
import { redirect, type ActionFunctionArgs } from "@remix-run/node";
import { Form } from "@remix-run/react";

export const action = async ({ request }: ActionFunctionArgs) => {
  const formData = await request.formData();
  const name = formData.get("name");
  const price = Number(formData.get("price"));

  // Lưu vào Database tại đây
  console.log("Saving product:", { name, price });

  return redirect("/products");
};

export default function NewProduct() {
  return (
    <Form method="post" className="space-y-4">
      <input type="text" name="name" placeholder="Tên sản phẩm" required />
      <input type="number" name="price" placeholder="Giá" required />
      <button type="submit">Lưu sản phẩm</button>
    </Form>
  );
}

Điểm tinh tế nhất: ngay khi action hoàn tất, Remix tự động “re-validate” lại toàn bộ dữ liệu trên trang. Danh sách sản phẩm sẽ được cập nhật mới nhất mà bạn không cần viết thêm bất kỳ dòng code đồng bộ state nào.

3. Thực tế hóa Progressive Enhancement

Hãy thử một thí nghiệm: tắt JavaScript trên trình duyệt và submit form trên. Bạn sẽ ngạc nhiên vì nó vẫn chạy cực mượt! Khi có JS, Remix hoạt động như một SPA nhanh nhạy. Khi không có JS, nó tự động fallback về cơ chế HTML Form truyền thống. Đây chính là cách xây dựng những ứng dụng “nồi đồng cối đá”, bất chấp mọi điều kiện mạng.

Vận hành và Giám sát: Để ứng dụng luôn “sống khỏe”

Việc deploy một ứng dụng Remix rất khác so với React thuần túy vì bạn cần một môi trường server (Node.js, Bun hoặc Edge Functions).

Xử lý lỗi thông minh với ErrorBoundary

Remix không để một lỗi nhỏ làm sập cả trang web. Với ErrorBoundary, nếu route con bị lỗi, chỉ phần đó bị thay thế bằng thông báo lỗi, các phần khác của trang web vẫn hoạt động bình thường.

export function ErrorBoundary() {
  return (
    <div className="p-4 bg-red-50 text-red-700">
      <h2>Hệ thống đang bận một chút!</h2>
      <p>Bạn thử làm mới trang hoặc quay lại sau vài phút nhé.</p>
    </div>
  );
}

Các chỉ số cần theo dõi

Vì logic nằm nhiều ở server, tốc độ của loader là yếu tố then chốt. Hãy chú ý các công cụ sau:

  • Vercel/Cloudflare Analytics: Theo dõi Real User Metrics (RUM) để biết người dùng thực tế đang trải nghiệm nhanh hay chậm.
  • Sentry: Bắt lỗi 500 ngay tại server-side trước khi người dùng kịp phàn nàn.
  • Lighthouse: Tập trung vào chỉ số LCP. Với Remix, LCP thường dưới 1.2s – một con số mơ ước của các ứng dụng SPA truyền thống.

Mẹo nhỏ: Luôn dùng Redis để cache các kết quả từ API bên thứ ba. Nếu loader mất hơn 500ms để phản hồi, người dùng sẽ cảm thấy trang web bị “khựng” khi chuyển đổi giữa các route.

Học Remix không chỉ là học thêm một công cụ, mà là định nghĩa lại cách làm sản phẩm web chuyên nghiệp: nhanh hơn, bảo mật hơn và bền bỉ hơn.

Share: