Best Practices for Building Applications with RemixJS and TypeScript

Ram Kumar

Ram Kumar

January 21, 20254 min read

Best Practices for Building Applications with RemixJS and TypeScript

RemixJS is a modern web framework designed to deliver high-performance applications. Its tight integration with React, excellent support for server-side rendering (SSR), and fine-tuned control over routing and data fetching make it an outstanding choice for building scalable web apps. In this post, we'll go over some key best practices for building applications using RemixJS with TypeScript, including examples to help you get started.

1. Leverage Nested Routing for Modularity and Performance

RemixJS supports nested routes, which allows for more granular control over data fetching and UI rendering. This modularity enables you to break your app into smaller, reusable components, improving maintainability and performance.

Best Practice

Use nested routes wherever possible, loading only the data needed for each route, and reducing unnecessary rendering.

Example

Here’s an example of using nested routes to create a dashboard:

// src/routes/dashboard.tsx
import { Link, Outlet } from "react-router-dom";

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <nav>
        <Link to="profile">Profile</Link>
        <Link to="settings">Settings</Link>
      </nav>
      <Outlet />
    </div>
  );
}

Then, create nested routes for profile and settings:

// src/routes/dashboard/profile.tsx
export default function Profile() {
  return <div>Profile Page</div>;
}
// src/routes/dashboard/settings.tsx
export default function Settings() {
  return <div>Settings Page</div>;
}

This setup allows the Dashboard component to act as a layout and load specific content for the profile or settings route as needed.

2. Optimize Data Fetching with Loaders

Remix uses loaders to fetch data on the server before rendering the page. This gives you full control over when and how data is fetched, improving SEO and performance.

Best Practice

Keep your loader logic minimal and avoid unnecessary re-fetching. Reuse loaders across routes when possible.

Example

Here’s how you can define a loader for fetching a list of users in a Remix route:

// src/routes/users.tsx
import { json, LoaderFunction } from "remix";
import { useLoaderData } from "react-router-dom";

export let loader: LoaderFunction = async () => {
  const res = await fetch("https://api.example.com/users");
  const users = await res.json();
  return json(users);
};

export default function Users() {
  let users = useLoaderData();

  return (
    <div>
      <h1>Users</h1>
      <ul>
        {users.map((user: { id: string; name: string }) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

In this example:

  • loader fetches data on the server before the page is rendered.
  • useLoaderData is used in the component to access the data.

3. Handle Form Submissions with Actions

Remix provides actions for handling POST requests and form submissions. Actions let you handle server-side logic, such as updating records, without a full page reload.

Best Practice

Use actions for form submissions and POST requests to keep your app state and server-side logic together.

Example

Let’s create a form for adding a new user, and handle the submission using an action:

// src/routes/users/new.tsx
import { json, ActionFunction } from "remix";
import { useNavigate } from "react-router-dom";

export let action: ActionFunction = async ({ request }) => {
  let formData = new URLSearchParams(await request.text());
  let name = formData.get("name");

  // Simulate creating a new user
  await fetch("https://api.example.com/users", {
    method: "POST",
    body: JSON.stringify({ name }),
  });

  return json({ success: true });
};

export default function NewUser() {
  let navigate = useNavigate();

  const handleSubmit = async (event: React.FormEvent) => {
    event.preventDefault();
    let form = new FormData(event.target as HTMLFormElement);
    let name = form.get("name");

    let res = await fetch("/users", {
      method: "POST",
      body: new URLSearchParams({ name: name as string }),
    });

    if (res.ok) {
      navigate("/users");
    }
  };

  return (
    <div>
      <h1>Add New User</h1>
      <form onSubmit={handleSubmit}>
        <input type="text" name="name" placeholder="Enter name" required />
        <button type="submit">Submit</button>
      </form>
    </div>
  );
}

Here:

  • The action function handles the form submission by making a POST request to an API.
  • The form's submission is handled on the server-side, ensuring proper data validation and a smooth user experience.

4. Optimize Asset Loading and Caching

Remix provides robust caching features and asset management. Caching assets like images and fonts, along with using Cache-Control headers, can drastically improve performance.

Best Practice

Use Cache-Control headers to manage the caching of dynamic content and static assets.

Example

You can set cache headers in your loader:

// src/routes/images.tsx
import { LoaderFunction, json } from "remix";

export let loader: LoaderFunction = async () => {
  let image = await fetch("https://api.example.com/image.jpg");
  let imageBlob = await image.blob();

  return json(imageBlob, {
    headers: { "Cache-Control": "public, max-age=31536000" },
  });
};

In this case:

  • The image is cached for one year by setting the Cache-Control header to max-age=31536000.

5. Handling Authentication with Sessions

Managing authentication is crucial for most applications. Remix offers easy-to-use sessions to manage user authentication states.

Best Practice

Store session data securely and use server-side cookies for sensitive data, such as authentication tokens.

Example

Here’s how you can create a simple session-based login:

// src/routes/login.tsx
import { json, LoaderFunction, ActionFunction } from "remix";
import { redirect } from "react-router-dom";

export let action: ActionFunction = async ({ request }) => {
  let formData = new URLSearchParams(await request.text());
  let username = formData.get("username");
  
  // Simulate a login check
  if (username === "admin") {
    // Set user session data
    return redirect("/dashboard", {
      headers: {
        "Set-Cookie": `user=${username}; Path=/; HttpOnly`,
      },
    });
  }

  return json({ error: "Invalid username" });
};

export default function Login() {
  return (
    <div>
      <h1>Login</h1>
      <form method="post">
        <input type="text" name="username" placeholder="Username" required />
        <button type="submit">Login</button>
      </form>
    </div>
  );
}

In this example:

  • The login action stores the session cookie (Set-Cookie) to track the authenticated user.

6. Use TypeScript for Type Safety

Remix is built with TypeScript in mind, and using it in your project will ensure strong typing throughout your app. This helps catch bugs early, improves the developer experience, and scales your project.

Best Practice

Ensure your loaders, actions, and components are properly typed to catch errors early.

Example

Here’s how you can type your loader function:

// src/routes/products.tsx
import { json, LoaderFunction } from "remix";

interface Product {
  id: number;
  name: string;
  price: number;
}

export let loader: LoaderFunction = async () => {
  const res = await fetch("https://api.example.com/products");
  const products: Product[] = await res.json();
  return json(products);
};

export default function Products() {
  const products = useLoaderData<Product[]>();

  return (
    <div>
      <h1>Products</h1>
      <ul>
        {products.map((product) => (
          <li key={product.id}>{product.name} - ${product.price}</li>
        ))}
      </ul>
    </div>
  );
}

In this case:

  • TypeScript ensures that products is correctly typed as an array of Product objects.

Conclusion

By following these RemixJS best practices, you can ensure that your application is fast, maintainable, and scalable. Using TypeScript adds an extra layer of security and productivity, preventing many common errors before they happen. Remix's combination of server-side rendering, nested routing, data-fetching mechanisms, and seamless navigation makes it an excellent choice for modern web development.

Previous: Advanced Guide to HTTP CRUD Operations Using Fetch API in Remix.js with TypeScript
Next: State Management in React: Comparing Redux, MobX, and Zustand (With TypeScript, Unit Tests, Performance & Memory Benchmarks)