Kuasai Remix dari dasar. Pelajari mengapa Remix diciptakan, pahami core concepts seperti file-based routing, loaders, actions, forms, dan data mutations. Bangun complete production-ready blog platform yang cover semua Remix fundamentals dengan best practices.

Remix revolutionize full-stack React development dengan provide framework yang embrace web fundamentals sambil leverage modern React patterns. Berbeda dengan traditional SPAs, Remix enable server-side rendering, progressive enhancement, dan seamless data mutations. Tapi mengapa Remix ada, dan apa yang membuatnya fundamentally different?
Dalam artikel ini, kita akan explore Remix's philosophy, understand mengapa Remix diciptakan, dive deep ke core concepts, dan build complete production-ready blog platform yang demonstrate semua fundamental Remix patterns.
Sebelum Remix, full-stack React developers faced significant challenges:
Ryan Florence dan Michael Jackson created Remix dengan revolutionary approach:
Remix menggunakan file-based routing similar ke Next.js tapi dengan different approach.
// File structure create routes automatically
app/
├── root.tsx // Root layout
├── routes/
│ ├── _index.tsx // / (home page)
│ ├── about.tsx // /about
│ ├── blog/
│ │ ├── _index.tsx // /blog
│ │ └── $slug.tsx // /blog/:slug (dynamic)
│ └── api/
│ └── posts.tsx // /api/posts (API route)
// Dynamic route dengan params
// app/routes/blog/$slug.tsx
import { useParams } from '@remix-run/react';
export default function BlogPost() {
const { slug } = useParams();
return <h1>Blog Post: {slug}</h1>;
}Loaders fetch data pada server sebelum rendering.
// app/routes/blog._index.tsx
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
export async function loader() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return json({ posts });
}
export default function BlogIndex() {
const { posts } = useLoaderData<typeof loader>();
return (
<div>
<h1>Blog Posts</h1>
<ul>
{posts.map(post => (
<li key={post.id}>
<a href={\`/blog/\${post.slug}\`}>{post.title}</a>
</li>
))}
</ul>
</div>
);
}Actions handle form submissions dan data mutations.
// app/routes/blog.new.tsx
import { json, redirect } from '@remix-run/node';
import { Form, useActionData } from '@remix-run/react';
export async function action({ request }: { request: Request }) {
if (request.method !== 'POST') {
return json({ error: 'Method not allowed' }, { status: 405 });
}
const formData = await request.formData();
const title = formData.get('title');
const content = formData.get('content');
// Validate
if (!title || !content) {
return json({ error: 'Title dan content are required' }, { status: 400 });
}
// Save ke database
const post = await fetch('https://api.example.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content }),
}).then(r => r.json());
return redirect(\`/blog/\${post.slug}\`);
}
export default function NewBlogPost() {
const actionData = useActionData<typeof action>();
return (
<Form method="post">
<div>
<label htmlFor="title">Title</label>
<input id="title" name="title" type="text" required />
</div>
<div>
<label htmlFor="content">Content</label>
<textarea id="content" name="content" required />
</div>
{actionData?.error && <p className="error">{actionData.error}</p>}
<button type="submit">Create Post</button>
</Form>
);
}HTML forms work tanpa JavaScript.
// app/routes/contact.tsx
import { json } from '@remix-run/node';
import { Form, useActionData, useNavigation } from '@remix-run/react';
export async function action({ request }: { request: Request }) {
if (request.method !== 'POST') {
return json({ error: 'Method not allowed' }, { status: 405 });
}
const formData = await request.formData();
const email = formData.get('email');
const message = formData.get('message');
// Send email
await sendEmail({ email, message });
return json({ success: true });
}
export default function Contact() {
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
const isSubmitting = navigation.state === 'submitting';
return (
<Form method="post">
<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" disabled={isSubmitting}>
{isSubmitting ? 'Sending...' : 'Send'}
</button>
{actionData?.success && <p>Message sent!</p>}
</Form>
);
}Create nested layouts dengan shared UI.
// app/routes/dashboard._layout.tsx
import { Outlet } from '@remix-run/react';
export default function DashboardLayout() {
return (
<div className="dashboard">
<aside className="sidebar">
<nav>
<a href="/dashboard">Overview</a>
<a href="/dashboard/posts">Posts</a>
<a href="/dashboard/settings">Settings</a>
</nav>
</aside>
<main className="content">
<Outlet />
</main>
</div>
);
}
// app/routes/dashboard._layout.posts.tsx
export default function Posts() {
return <h1>Posts</h1>;
}
// app/routes/dashboard._layout.settings.tsx
export default function Settings() {
return <h1>Settings</h1>;
}Handle errors gracefully dengan error boundaries.
// app/routes/blog.$slug.tsx
import { useRouteError, isRouteErrorResponse } from '@remix-run/react';
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div>
<h1>{error.status} {error.statusText}</h1>
<p>{error.data}</p>
</div>
);
}
return (
<div>
<h1>Error</h1>
<p>{error instanceof Error ? error.message : 'Unknown error'}</p>
</div>
);
}
export default function BlogPost() {
return <h1>Blog Post</h1>;
}Manage metadata untuk each route.
// app/routes/blog.$slug.tsx
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
export async function loader({ params }: { params: { slug: string } }) {
const post = await fetch(\`https://api.example.com/posts/\${params.slug}\`).then(r => r.json());
return json({ post });
}
export const meta = ({ data }: { data: typeof loader }) => {
return [
{ title: data.post.title },
{ name: 'description', content: data.post.excerpt },
{ property: 'og:title', content: data.post.title },
{ property: 'og:description', content: data.post.excerpt },
{ property: 'og:image', content: data.post.image },
];
};
export default function BlogPost() {
const { post } = useLoaderData<typeof loader>();
return <h1>{post.title}</h1>;
}Handle authentication dan sessions.
// app/sessions.server.ts
import { createCookieSessionStorage } from '@remix-run/node';
export const sessionStorage = createCookieSessionStorage({
cookie: {
name: '__session',
httpOnly: true,
maxAge: 60 * 60 * 24 * 7,
path: '/',
sameSite: 'lax',
secrets: [process.env.SESSION_SECRET!],
secure: process.env.NODE_ENV === 'production',
},
});
export const { getSession, commitSession, destroySession } = sessionStorage;
// app/routes/login.tsx
import { json, redirect } from '@remix-run/node';
import { Form } from '@remix-run/react';
import { getSession, commitSession } from '~/sessions.server';
export async function action({ request }: { request: Request }) {
if (request.method !== 'POST') {
return json({ error: 'Method not allowed' }, { status: 405 });
}
const formData = await request.formData();
const email = formData.get('email');
const password = formData.get('password');
// Authenticate user
const user = await authenticateUser(email, password);
if (!user) {
return json({ error: 'Invalid credentials' }, { status: 401 });
}
const session = await getSession();
session.set('userId', user.id);
return redirect('/dashboard', {
headers: {
'Set-Cookie': await commitSession(session),
},
});
}
export default function Login() {
return (
<Form method="post">
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Login</button>
</Form>
);
}Create API endpoints tanpa separate backend.
// app/routes/api.posts.tsx
import { json } from '@remix-run/node';
export async function loader() {
const posts = await getPosts();
return json(posts);
}
export async function action({ request }: { request: Request }) {
if (request.method === 'POST') {
const data = await request.json();
const post = await createPost(data);
return json(post, { status: 201 });
}
return json({ error: 'Method not allowed' }, { status: 405 });
}Update UI optimistically sambil wait untuk server response.
// app/routes/posts.$id.edit.tsx
import { Form, useNavigation, useLoaderData } from '@remix-run/react';
export default function EditPost() {
const { post } = useLoaderData<typeof loader>();
const navigation = useNavigation();
// Optimistic UI - show new values immediately
const formData = navigation.formData;
const optimisticTitle = formData?.get('title') ?? post.title;
const optimisticContent = formData?.get('content') ?? post.content;
return (
<Form method="post">
<input
name="title"
defaultValue={optimisticTitle}
disabled={navigation.state === 'submitting'}
/>
<textarea
name="content"
defaultValue={optimisticContent}
disabled={navigation.state === 'submitting'}
/>
<button type="submit" disabled={navigation.state === 'submitting'}>
{navigation.state === 'submitting' ? 'Saving...' : 'Save'}
</button>
</Form>
);
}Mari build complete blog platform dengan Remix.
blog-app/
├── app/
│ ├── root.tsx
│ ├── sessions.server.ts
│ ├── routes/
│ │ ├── _index.tsx
│ │ ├── blog/
│ │ │ ├── _index.tsx
│ │ │ └── $slug.tsx
│ │ ├── blog.new.tsx
│ │ ├── blog.$id.edit.tsx
│ │ ├── login.tsx
│ │ ├── logout.tsx
│ │ ├── dashboard._layout.tsx
│ │ ├── dashboard._layout.posts.tsx
│ │ └── api/
│ │ └── posts.tsx
│ ├── components/
│ │ ├── Header.tsx
│ │ ├── PostCard.tsx
│ │ └── PostForm.tsx
│ ├── styles/
│ │ └── globals.css
│ └── utils/
│ └── db.server.ts
├── public/
├── remix.config.js
├── package.json
└── tsconfig.jsonImplementasi untuk root layout, routes, components, dan database utilities sama dengan English version. Struktur dan logic tetap sama, hanya dengan Indonesian comments dan labels.
// ✅ Keep loaders focused pada data fetching
// ✅ Keep actions focused pada mutations
// ✅ Use proper HTTP methods (GET, POST, PUT, DELETE)
// ✅ Return appropriate status codes
// ❌ Avoid mixing data fetching dan mutations
// ❌ Don't ignore HTTP semantics
// ❌ Avoid complex logic dalam loaders/actions// ✅ Use HTML forms untuk progressive enhancement
// ✅ Validate pada both client dan server
// ✅ Show loading states during submission
// ✅ Handle errors gracefully
// ❌ Avoid JavaScript-only forms
// ❌ Don't skip server-side validation
// ❌ Avoid poor error messages// ✅ Use error boundaries untuk route errors
// ✅ Throw responses untuk expected errors
// ✅ Provide meaningful error messages
// ✅ Handle 404s dan other status codes
// ❌ Avoid silent failures
// ❌ Don't expose sensitive error details
// ❌ Avoid generic error messages// ✅ Use parallel data loading dengan loaders
// ✅ Implement proper caching strategies
// ✅ Use resource routes untuk API endpoints
// ✅ Optimize database queries
// ❌ Avoid sequential data loading
// ❌ Don't fetch unnecessary data
// ❌ Avoid N+1 query problems// ✅ Validate semua user input
// ✅ Check authorization dalam actions
// ✅ Use secure session management
// ✅ Implement CSRF protection
// ❌ Don't trust user input
// ❌ Avoid exposing sensitive data
// ❌ Don't skip authentication checks// ❌ Wrong - fetching dalam component
export default function Posts() {
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch('/api/posts').then(r => r.json()).then(setPosts);
}, []);
return <div>{/* render posts */}</div>;
}
// ✅ Correct - fetch dalam loader
export async function loader() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return json({ posts });
}
export default function Posts() {
const { posts } = useLoaderData<typeof loader>();
return <div>{/* render posts */}</div>;
}// ❌ Wrong - using fetch untuk mutations
<button onClick={() => fetch('/api/posts', { method: 'POST' })}>
Create
</button>
// ✅ Correct - using forms
<Form method="post">
<input name="title" />
<button type="submit">Create</button>
</Form>// ❌ Wrong - no error handling
export default function Post() {
return <h1>Post</h1>;
}
// ✅ Correct - include error boundary
export function ErrorBoundary() {
const error = useRouteError();
return <div>Error: {error.message}</div>;
}
export default function Post() {
return <h1>Post</h1>;
}// ❌ Wrong - hanya client validation
<input required />
// ✅ Correct - validate pada server
export async function action({ request }: { request: Request }) {
const formData = await request.formData();
const title = formData.get('title');
if (!title) {
return json({ error: 'Title is required' }, { status: 400 });
}
// Process...
}// ❌ Wrong - sequential requests
export async function loader() {
const posts = await getPosts();
const comments = await getComments(); // Waits untuk posts
return json({ posts, comments });
}
// ✅ Correct - parallel requests
export async function loader() {
const [posts, comments] = await Promise.all([
getPosts(),
getComments(),
]);
return json({ posts, comments });
}# Install Vercel CLI
npm i -g vercel
# Deploy
vercel
# Deploy ke production
vercel --prod# Install Fly CLI
curl -L https://fly.io/install.sh | sh
# Launch app
fly launch
# Deploy
fly deployRemix revolutionize full-stack React development dengan embrace web fundamentals sambil leverage modern React patterns. Dengan combine server-side rendering, progressive enhancement, dan declarative data fetching, Remix enable developers untuk build fast, resilient, dan user-friendly applications.
Blog platform yang kita build demonstrate semua core Remix concepts dalam action. Understanding loaders, actions, forms, error boundaries, dan progressive enhancement adalah essential untuk building modern Remix applications.
Key takeaways:
Next steps:
Remix adalah future of full-stack React development. Keep learning, building, dan pushing boundaries dari apa yang possible.