Remix.js: Building “Standard” Web Apps – From Data Loading to Server Actions

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

Remix.js: A Fresh Breeze or a Return to Core Values?

If you’ve ever built a Single Page Application (SPA) with React, you’re likely no stranger to the sight of a loading spinner whirling indefinitely every time you switch pages. Data is typically fetched on the client side after the component mounts. This approach creates experience gaps, especially when users are accessing your site via a weak 4G connection at a cafe or on a high-speed train.

I used to spend weeks struggling with useEffect, fighting race conditions, and configuring bulky state management libraries. Everything changed when I tried Remix.js. Instead of inventing dozens of complex new concepts, Remix chooses to go back in time, fully leveraging Web Standards that are incredibly powerful yet often forgotten.

The core philosophy of Remix is Progressive Enhancement. Simply put: your application must work even if JavaScript fails or hasn’t finished loading. This approach not only makes the site instantly responsive but is also a “booster” for SEO—a factor determining revenue that developers sometimes overlook.

Remix completely eliminates “Waterfall fetching” (overlapping requests) using loaders that run in parallel on the server. You no longer have to worry about exposing secret API keys or stuffing heavy data processing logic into the user’s browser.

Project Initialization: Quick, Clean, and Standards-Compliant

To get started, ensure you have Node.js v18+ installed. Just one command sets everything up:

npx create-remix@latest my-remix-app

During installation, I usually prefer the following options to make future development “breathable”:

  • Directory: Name your project clearly, e.g., remix-ecommerce-v1.
  • Install dependencies: Select Yes (use npm or pnpm for speed).
  • TypeScript: Always select Yes. Remix’s ability to automatically infer data types from the loader to the component is incredibly powerful, helping you avoid silly “undefined” errors.
cd my-remix-app
npm run dev

The app will start at localhost:3000. Remix’s folder structure is very intuitive; most of the time you’ll be working in the app/ directory—where the main routes and logic reside.

The Secret to Mastering Loaders, Actions, and Progressive Enhancement

This is the trio that sets Remix apart. Let’s see how we handle a product list page in practice.

1. Data Loading with Loaders (Server-side fetch)

Each route file in Remix acts as a small API endpoint. The loader function runs only on the server, allowing you to query the Database directly without going through an intermediate API layer.

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

export const loader = async () => {
  // Fetch data from DB, e.g., the 10 latest products
  const products = [
    { id: 1, name: "Akko Mechanical Keyboard", price: 1200000 },
    { id: 2, name: "Logitech G502 Mouse", price: 850000 },
  ];
  return json({ products });
};

export default function ProductsPage() {
  const { products } = useLoaderData<typeof loader>();
  
  return (
    <div>
      <h1 className="text-2xl font-bold">Hot Products</h1>
      <ul>
        {products.map(p => (
          <li key={p.id}>{p.name} - {p.price.toLocaleString()} VND</li>
        ))}
      </ul>
    </div>
  );
}

A painful lesson learned: when working with multi-level nested JSON from Shopify or Contentful, data can easily get messy. I often use JSON Formatter to clean and inspect the data structure before defining TypeScript Interfaces—saving at least 15 minutes every time I debug.

2. Data Mutation with Server Actions

Forget long-winded useState or onSubmit. Remix brings us back to the divine <Form> tag, but much more powerful. When you hit submit, the action function on the server receives and processes the data.

// 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"));

  // Save to Database here
  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="Product Name" required />
      <input type="number" name="price" placeholder="Price" required />
      <button type="submit">Save Product</button>
    </Form>
  );
}

The finest point: as soon as the action completes, Remix automatically “re-validates” all data on the page. The product list will be updated with the latest info without you having to write a single line of state synchronization code.

3. Realizing Progressive Enhancement

Try an experiment: turn off JavaScript in your browser and submit that form. You’ll be surprised it still works perfectly! With JS, Remix acts as a snappy SPA. Without JS, it automatically falls back to the traditional HTML Form mechanism. This is how you build “bulletproof” apps regardless of network conditions.

Operation and Monitoring: Keeping Your App Healthy

Deploying a Remix app is quite different from pure React because you need a server environment (Node.js, Bun, or Edge Functions).

Smart Error Handling with ErrorBoundary

Remix doesn’t let a small error crash the entire site. With ErrorBoundary, if a child route fails, only that section is replaced by an error message, while other parts of the site continue to function normally.

export function ErrorBoundary() {
  return (
    <div className="p-4 bg-red-50 text-red-700">
      <h2>The system is a bit busy!</h2>
      <p>Please try refreshing the page or come back in a few minutes.</p>
    </div>
  );
}

Metrics to Watch

Since logic sits heavily on the server, the speed of the loader is key. Pay attention to these tools:

  • Vercel/Cloudflare Analytics: Track Real User Metrics (RUM) to see if actual users are experiencing speed or lag.
  • Sentry: Catch 500 errors on the server side before users even have a chance to complain.
  • Lighthouse: Focus on the LCP metric. With Remix, LCP is often under 1.2s—a dream figure for traditional SPA apps.

Pro tip: Always use Redis to cache results from third-party APIs. If a loader takes more than 500ms to respond, users will feel the site “stutter” when navigating between routes.

Learning Remix isn’t just about learning another tool; it’s about redefining how to build professional web products: faster, more secure, and more resilient.

Share: