Remix is a full-stack web framework that lets you focus on the user interface. It combines the speed of server-side rendering with the interactivity of client-side JavaScript. This guide will get you up and running with Remix.
What is Remix?
Remix is built on:
- Web Standards - Uses the Web Fetch API, Request, and Response objects
- React - Modern React for building UIs
- Nested Routing - Route-based architecture for data loading
- Server-Side Rendering - Fast initial page loads and SEO
- Progressive Enhancement - Works without JavaScript
Installation
Creating a New Project
# Create a new Remix project
npx create-remix@latest
# Or with specific template
npx create-remix@latest my-app --template remix-run/remix/templates/remix
# Navigate and install
cd my-app
npm install
# Start development server
npm run devProject Structure
my-remix-app/
├── app/
│ ├── routes/ # Route files
│ │ ├── _index.tsx # Home route
│ │ └── about.tsx # About route
│ ├── root.tsx # Root component
│ ├── entry.client.tsx # Browser entry
│ ├── entry.server.tsx # Server entry
│ └── styles.css # Global styles
├── public/ # Static assets
├── package.json
├── remix.config.js # Remix configuration
└── tsconfig.json # TypeScript configCore Concepts
Routes
Routes are the heart of Remix. Each file in app/routes becomes a route:
// app/routes/_index.tsx
export default function Index() {
return (
<div>
<h1>Welcome to Remix!</h1>
</div>
);
}Route File Naming
_index.tsx- Index route (/`)about.tsx- /aboutblog._index.tsx- /blogblog.$slug.tsx- /blog/:slugfiles.$.tsx- Wildcard route (/files/*)
Nested Routing
Remix uses nested routing for layout and data:
// app/root.tsx
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from '@remix-run/react';
export default function App() {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/blog">Blog</a>
</nav>
</header>
<main>
<Outlet /> {/* Child routes render here */}
</main>
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}// app/routes/dashboard.tsx
import { Outlet, Link } from '@remix-run/react';
export default function Dashboard() {
return (
<div className="dashboard">
<aside>
<nav>
<Link to="/dashboard/overview">Overview</Link>
<Link to="/dashboard/settings">Settings</Link>
<Link to="/dashboard/analytics">Analytics</Link>
</nav>
</aside>
<div className="dashboard-content">
<h1>Dashboard</h1>
<Outlet /> {/* Nested routes */}
</div>
</div>
);
}// app/routes/dashboard.overview.tsx
export default function Overview() {
return (
<div>
<h2>Overview</h2>
<p>Welcome to your dashboard overview.</p>
</div>
);
}Loaders
Loaders run on the server to provide data to your route:
// app/routes/posts.tsx
import { useLoaderData, Link } from '@remix-run/react';
import { json } from '@remix-run/node';
// Loader runs on the server
export const loader = async () => {
const posts = [
{ id: 1, title: 'First Post', slug: 'first-post' },
{ id: 2, title: 'Second Post', slug: 'second-post' },
];
return json(posts);
};
export default function Posts() {
const posts = useLoaderData<typeof loader>();
return (
<div>
<h1>Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link to={post.slug}>{post.title}</Link>
</li>
))}
</ul>
</div>
);
}Fetching Data from APIs
import { json } from '@remix-run/node';
export const loader = async () => {
const response = await fetch('https://api.example.com/posts');
if (!response.ok) {
throw new Response('Failed to fetch posts', { status: 500 });
}
const posts = await response.json();
return json(posts);
};Accessing Request Data
import { json } from '@remix-run/node';
export const loader = async ({ request }: LoaderArgs) => {
const url = new URL(request.url);
const searchTerm = url.searchParams.get('q');
const posts = await getPosts(searchTerm);
return json(posts);
};Using Environment Variables
const API_URL = process.env.API_URL || 'http://localhost:3000';
export const loader = async () => {
const response = await fetch(`${API_URL}/posts`);
const posts = await response.json();
return json(posts);
};Actions
Actions handle form submissions:
// app/routes/contact.tsx
import { Form, useActionData } from '@remix-run/react';
import { json, redirect } from '@remix-run/node';
import { action } from '.';
export const action = async ({ request }: ActionArgs) => {
const formData = await request.formData();
const name = formData.get('name');
const email = formData.get('email');
const message = formData.get('message');
// Validate
if (!name || !email || !message) {
return json({ error: 'All fields are required' }, { status: 400 });
}
// Save to database
await saveContact({ name, email, message });
// Redirect
return redirect('/contact/success');
};
export default function Contact() {
const actionData = useActionData<typeof action>();
return (
<div>
<h1>Contact Us</h1>
{actionData?.error && (
<p className="error">{actionData.error}</p>
)}
<Form method="post">
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" type="text" required />
</div>
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" required />
</div>
<div>
<label htmlFor="message">Message</label>
<textarea id="message" name="message" required />
</div>
<button type="submit">Send</button>
</Form>
</div>
);
}Error Handling
Catching Errors
// app/routes/posts.$postId.tsx
import { useLoaderData, useRouteError } from '@remix-run/react';
export const loader = async ({ params }: LoaderArgs) => {
const post = await getPost(params.postId);
if (!post) {
throw new Response('Not Found', { status: 404 });
}
return json(post);
};
export default function Post() {
const post = useLoaderData<typeof loader>();
return <div>{post.title}</div>;
}
export function ErrorBoundary() {
const error = useRouteError();
if (error instanceof Response && error.status === 404) {
return <div>Post not found</div>;
}
return <div>Something went wrong</div>;
}Styling
Using CSS
// app/routes/about.tsx
import styles from '~/styles/about.css?url';
export function links() {
return [{ rel: 'stylesheet', href: styles }];
}
export default function About() {
return (
<div className="about">
<h1>About Us</h1>
<p>Learn more about our company.</p>
</div>
);
}Using Tailwind CSS
export default function Index() {
return (
<div className="container mx-auto px-4">
<h1 className="text-4xl font-bold">Welcome</h1>
</div>
);
}Client-Side Interactivity
Using useEffect
import { useEffect } from 'react';
export default function Clock() {
const [time, setTime] = useState(new Date());
useEffect(() => {
const interval = setInterval(() => {
setTime(new Date());
}, 1000);
return () => clearInterval(interval);
}, []);
return <div>{time.toLocaleTimeString()}</div>;
}Client Components
import { ClientOnly } from '@remix-run/react';
export default function InteractiveMap() {
return (
<ClientOnly fallback={<p>Loading map...</p>}>
{() => <MapComponent />}
</ClientOnly>
);
}Deployment
Building for Production
# Build the app
npm run build
# Start production server
npm startEnvironment Variables
Create a .env file:
DATABASE_URL=postgresql://...
SESSION_SECRET=your-secret-key
API_KEY=your-api-keyAccess in code:
const DATABASE_URL = process.env.DATABASE_URL;Best Practices
- 1Use Nested Routes - Leverage nested routing for layouts
- 2Server-Side First - Load data on the server when possible
- 3Error Boundaries - Always include error boundaries
- 4Progressive Enhancement - Ensure forms work without JS
- 5TypeScript - Use TypeScript for better DX
- 6Optimize Images - Use Remix's image optimization
Conclusion
Remix provides a modern, standards-based approach to building web applications. By combining the speed of server-side rendering with the interactivity of client-side JavaScript, it offers the best of both worlds.
Start with the basics: understand routes, loaders, and actions. Then explore nested routing, error handling, and deployment strategies. With Remix, you get fast initial loads, excellent SEO, and great user experiences.