🎯 empfohlene Sammlungen
Balanced sample collections from various categories for you to explore
Remix Beispiele
Remix Full-Stack Web-Framework Beispiele einschließlich Routen, Loader, Actions und moderne Web-Entwicklungsmuster
💻 Grundlegende Remix-Anwendung typescript
🟢 simple
⭐⭐
Essentielle Remix-App-Struktur mit Routing, Datenladung und Formularverarbeitung
⏱️ 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 },
];
export default 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";
export default 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 });
}
export default 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}`);
}
export default 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";
export default 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 },
});
}
💻 Erweiterte Remix-Muster typescript
🟡 intermediate
⭐⭐⭐⭐
Komplexe Remix-Muster einschließlich Authentifizierung, Caching und Leistungsoptimierung
⏱️ 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),
},
});
}
export default 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"