Exemples Remix
Exemples du framework web full-stack Remix incluant les routes, chargeurs, actions et patterns modernes de développement web
Key Facts
- Category
- Web Frameworks
- Items
- 2
- Format Families
- sample
Sample Overview
Exemples du framework web full-stack Remix incluant les routes, chargeurs, actions et patterns modernes de développement web This sample set belongs to Web Frameworks and can be used to test related workflows inside Elysia Tools.
💻 Application Remix de Base typescript
🟢 simple
⭐⭐
Structure d'application Remix essentielle avec routage, chargement de données et gestion de formulaires
⏱️ 25 min
🏷️ remix, full-stack, react, typescript
Prerequisites:
React basics, TypeScript, Node.js
// Remix Basic Application Examples
// Remix is a full-stack React framework focused on web fundamentals
// 1. Root Route (app/root.tsx)
import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from "@remix-run/react";
import type { LinksFunction } from "@remix-run/node";
import globalStylesheetUrl from "./styles/global.css";
export const links: LinksFunction = () => [
{ rel: "stylesheet", href: globalStylesheetUrl },
];
function App() {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
// 2. Index Route (app/routes/_index.tsx)
import { Link } from "@remix-run/react";
function Index() {
return (
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.4" }}>
<h1>Welcome to Remix</h1>
<ul>
<li>
<Link to="/posts">View Posts</Link>
</li>
<li>
<Link to="/posts/new">Create New Post</Link>
</li>
</ul>
</div>
);
}
// 3. Dynamic Route with Loader (app/routes/posts.$postId.tsx)
import { json, LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
// Mock database
const posts = [
{ id: "1", title: "First Post", body: "This is the first post content" },
{ id: "2", title: "Second Post", body: "This is the second post content" },
];
export async function loader({ params }: LoaderFunctionArgs) {
const post = posts.find(p => p.id === params.postId);
if (!post) {
throw new Response("Not Found", { status: 404 });
}
return json({ post });
}
function PostRoute() {
const { post } = useLoaderData<typeof loader>();
return (
<div>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
);
}
// 4. Form with Action (app/routes/posts.new.tsx)
import { ActionFunctionArgs, redirect } from "@remix-run/node";
import { Form } from "@remix-run/react";
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const title = formData.get("title");
const body = formData.get("body");
if (!title || !body) {
return json({ error: "Title and body are required" }, { status: 400 });
}
// In a real app, you would save to database here
const newPost = {
id: Date.now().toString(),
title: title.toString(),
body: body.toString(),
};
return redirect(`/posts/${newPost.id}`);
}
function NewPost() {
return (
<div>
<h1>Create New Post</h1>
<Form method="post">
<div>
<label htmlFor="title">Title: </label>
<input id="title" name="title" type="text" required />
</div>
<div>
<label htmlFor="body">Body: </label>
<textarea id="body" name="body" required />
</div>
<button type="submit">Create Post</button>
</Form>
</div>
);
}
// 5. API Route (app/routes/api/posts.ts)
import { json, LoaderFunctionArgs } from "@remix-run/node";
export async function loader({ request }: LoaderFunctionArgs) {
const posts = [
{ id: "1", title: "First Post", body: "This is the first post" },
{ id: "2", title: "Second Post", body: "This is the second post" },
];
const url = new URL(request.url);
const format = url.searchParams.get("format");
if (format === "json") {
return json({ posts });
}
return json({ posts });
}
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const post = {
id: Date.now().toString(),
title: formData.get("title"),
body: formData.get("body"),
};
return json({ post }, { status: 201 });
}
// 6. Error Boundary (app/routes/posts.$postId.tsx)
import { isRouteErrorResponse, useRouteError } from "@remix-run/react";
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div>
<h1>
{error.status} {error.statusText}
</h1>
<p>{error.data?.message}</p>
</div>
);
} else if (error instanceof Error) {
return (
<div>
<h1>Error</h1>
<p>{error.message}</p>
<p>The stack trace is:</p>
<pre>{error.stack}</pre>
</div>
);
} else {
return <h1>Unknown Error</h1>;
}
}
// 7. Layout Route (app/routes/posts.tsx)
import { Outlet } from "@remix-run/react";
import { Link } from "@remix-run/react";
function PostsLayout() {
return (
<div>
<nav>
<ul>
<li><Link to="/posts">All Posts</Link></li>
<li><Link to="/posts/new">New Post</Link></li>
</ul>
</nav>
<main>
<Outlet />
</main>
</div>
);
}
// 8. Resource Route (app/routes/resources/download.ts)
import { LoaderFunctionArgs } from "@remix-run/node";
export async function loader({ request }: LoaderFunctionArgs) {
// Generate a CSV file
const csvContent = "name,email\nJohn Doe,[email protected]\nJane Smith,[email protected]";
return new Response(csvContent, {
headers: {
"Content-Type": "text/csv",
"Content-Disposition": 'attachment; filename="users.csv"',
},
});
}
// 9. Using Sessions (app/utils/sessions.ts)
import { createCookieSessionStorage, redirect } from "@remix-run/node";
const { getSession, commitSession, destroySession } = createCookieSessionStorage({
cookie: {
name: "__session",
secrets: ["your-secret-key"],
sameSite: "lax",
path: "/",
maxAge: 60 * 60 * 24 * 30, // 30 days
httpOnly: true,
},
});
export async function requireAuth(request: Request) {
const session = await getSession(request.headers.get("Cookie"));
const userId = session.get("userId");
if (!userId) {
throw redirect("/login");
}
return userId;
}
export { getSession, commitSession, destroySession };
// 10. Database Integration (app/models/post.server.ts)
import { prisma } from "./db.server";
export async function getPosts() {
return prisma.post.findMany({
orderBy: { createdAt: "desc" },
});
}
export async function getPost(id: string) {
return prisma.post.findUnique({
where: { id },
});
}
export async function createPost(data: { title: string; body: string }) {
return prisma.post.create({
data,
});
}
export async function updatePost(id: string, data: { title?: string; body?: string }) {
return prisma.post.update({
where: { id },
data,
});
}
export async function deletePost(id: string) {
return prisma.post.delete({
where: { id },
});
}
💻 Patterns Avancés Remix typescript
🟡 intermediate
⭐⭐⭐⭐
Patterns complexes Remix incluant authentification, cache et optimisation des performances
⏱️ 45 min
🏷️ remix, advanced, patterns, full-stack
Prerequisites:
Remix basics, Node.js, TypeScript, Authentication concepts
// Advanced Remix Patterns
// 1. Authentication with Loader and Action
import { json, LoaderFunctionArgs, ActionFunctionArgs, redirect } from "@remix-run/node";
import { Form, useLoaderData } from "@remix-run/react";
import { getSession, commitSession } from "~/utils/sessions";
// Protected Route Loader
export async function loader({ request }: LoaderFunctionArgs) {
const session = await getSession(request.headers.get("Cookie"));
const userId = session.get("userId");
if (!userId) {
throw redirect("/login");
}
const user = await getUserById(userId);
if (!user) {
throw redirect("/login");
}
return json({ user });
}
// Login Action
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const email = formData.get("email");
const password = formData.get("password");
const user = await authenticateUser(email.toString(), password.toString());
if (!user) {
return json({ error: "Invalid credentials" }, { status: 400 });
}
const session = await getSession();
session.set("userId", user.id);
return redirect("/dashboard", {
headers: {
"Set-Cookie": await commitSession(session),
},
});
}
function Dashboard() {
const { user } = useLoaderData<typeof loader>();
return (
<div>
<h1>Welcome, {user.name}!</h1>
<Form method="post" action="/logout">
<button type="submit">Logout</button>
</Form>
</div>
);
}
// 2. Caching with Headers
export async function loader({ request }: LoaderFunctionArgs) {
const data = await getExpensiveData();
return json(data, {
headers: {
"Cache-Control": "public, max-age=3600, s-maxage=3600",
"Vary": "Cookie",
},
});
}
// 3. File Uploads
import { unstable_createFileUploadHandler } from "@remix-run/node";
import { unstable_createMemoryUploadHandler } from "@remix-run/node";
import { unstable_parseMultipartFormData } from "@remix-run/node";
const uploadHandler = unstable_createFileUploadHandler({
directory: "./public/uploads",
maxPartSize: 5_000_000, // 5MB
});
export async function action({ request }: ActionFunctionArgs) {
const formData = await unstable_parseMultipartFormData(
request,
uploadHandler
);
const file = formData.get("file") as File;
if (!file) {
return json({ error: "No file uploaded" }, { status: 400 });
}
return json({
message: "File uploaded successfully",
filename: file.name,
size: file.size
});
}
// 4. Real-time Updates with Server-Sent Events
export async function loader({ request }: LoaderFunctionArgs) {
return new Response(
new ReadableStream({
start(controller) {
const interval = setInterval(async () => {
const data = await getLatestData();
controller.enqueue(`data: ${JSON.stringify(data)}\n\n`);
}, 1000);
request.signal.addEventListener("abort", () => {
clearInterval(interval);
controller.close();
});
},
}),
{
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
}
);
}
// 5. Internationalization (i18n)
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const locale = url.searchParams.get("locale") || "en";
const messages = await import(`~/locales/${locale}.json`);
return json({
locale,
messages: messages.default
});
}
// 6. GraphQL Integration
import { GraphQLClient, gql } from "graphql-request";
const graphQLClient = new GraphQLClient(process.env.GRAPHQL_ENDPOINT!);
const GET_POSTS_QUERY = gql`
query GetPosts($limit: Int!) {
posts(limit: $limit) {
id
title
content
author {
name
}
}
}
`;
export async function loader() {
const data = await graphQLClient.request(GET_POSTS_QUERY, { limit: 10 });
return json(data);
}
// 7. Optimistic Updates
import { useFetcher } from "@remix-run/react";
function LikeButton({ post, liked }: { post: Post; liked: boolean }) {
const fetcher = useFetcher();
const isLiked = fetcher.formData ?
fetcher.formData.get("liked") === "true" :
liked;
return (
<fetcher.Form method="post" action="/api/like">
<input type="hidden" name="postId" value={post.id} />
<input type="hidden" name="liked" value={(!isLiked).toString()} />
<button type="submit">
{isLiked ? "❤️" : "🤍"} {post.likes}
</button>
</fetcher.Form>
);
}
// 8. Progressive Enhancement
function SearchForm({ query }: { query?: string }) {
return (
<Form method="get" action="/search" className="search-form">
<input
type="search"
name="q"
defaultValue={query}
placeholder="Search posts..."
required
/>
<button type="submit">Search</button>
</Form>
);
}
// 9. Custom Server (server.js)
import { createRequestHandler } from "@remix-run/express";
import express from "express";
const app = express();
app.use(express.static("public", { maxAge: "1h" }));
app.all(
"*",
createRequestHandler({
build: require("@remix-run/dev/server-build"),
})
);
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Express server listening on port ${port}`);
});
// 10. Testing with Vitest
import { test, expect } from "vitest";
import { loader } from "~/routes/posts._index";
import { createRequest } from "@remix-run/node";
test("posts loader returns posts", async () => {
const request = createRequest("http://localhost:3000/posts");
const response = await loader({ request, params: {}, context: {} });
const data = await response.json();
expect(data.posts).toBeInstanceOf(Array);
expect(data.posts[0]).toHaveProperty("title");
});
// 11. Deployment Configuration (vercel.json)
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/static-build",
"config": {
"distDir": "public"
}
},
{
"src": "server.js",
"use": "@vercel/node"
}
],
"routes": [
{
"src": "/(.*)",
"dest": "server.js"
}
]
}
// 12. Environment Variables Management (.env.example)
DATABASE_URL="postgresql://user:password@localhost:5432/myapp"
SESSION_SECRET="your-session-secret"
REDIS_URL="redis://localhost:6379"
UPLOAD_DIR="./public/uploads"
NODE_ENV="development"