Fiszki Online Remix (Preview)
Darmowy podgląd 15 z 40 dostępnych pytań
Remix - Sekcja 1: Podstawy i Architektura
1. Czym jest Remix i jakie problemy rozwiązuje w porównaniu do innych frameworków React?
Odpowiedź w 30 sekund: Remix to full-stack framework React oparty na Web Standards, który przenosi renderowanie na serwer i wykorzystuje natywne możliwości platformy web. Rozwiązuje problemy związane z wydajnością ładowania stron, obsługą formularzy, zarządzaniem stanem serwer-klient oraz progresywnym wzbogacaniem funkcjonalności bez konieczności budowania skomplikowanych rozwiązań po stronie klienta.
Odpowiedź w 2 minuty: Remix to framework React stworzony przez twórców React Router, który skupia się na wykorzystaniu natywnych możliwości platformy web zamiast budowania własnych abstrakcji. Główne problemy, które rozwiązuje to: nadmierne poleganie na JavaScript po stronie klienta, skomplikowane zarządzanie stanem między serwerem a klientem, problemy z wydajnością initial load oraz nieintuicyjna obsługa formularzy i mutacji danych.
W przeciwieństwie do tradycyjnych SPA, Remix domyślnie renderuje wszystko na serwerze i wysyła gotowy HTML do przeglądarki, a następnie wzbogaca go o JavaScript (progressive enhancement). Wykorzystuje standardowe Web API jak Request, Response, Headers i FormData, co oznacza że developer nie musi uczyć się specyficznych abstrakcji frameworka - wystarczy znajomość standardów web.
Remix rozwiązuje też problem "data waterfalls" poprzez równoległe ładowanie danych na poziomie zagnieżdżonych tras. Framework automatycznie zarządza rewalidacją danych po mutacjach, eliminując potrzebę ręcznego synchronizowania stanu cache. Dodatkowo, formularze działają nawet bez JavaScript, co zapewnia lepszą dostępność i odporność aplikacji.
Kluczową różnicą jest filozofia: zamiast budować skomplikowane state management solutions po stronie klienta, Remix wykorzystuje serwer jako źródło prawdy i minimalizuje stan klienta do niezbędnego minimum.
Przykład kodu:
// Tradycyjny loader w Remix - zwykła funkcja serwerowa
export async function loader({ request }: LoaderFunctionArgs) {
// Wykorzystanie standardowego Web API
const url = new URL(request.url);
const searchTerm = url.searchParams.get("q");
// Pobieranie danych z bazy
const products = await db.product.findMany({
where: { name: { contains: searchTerm } }
});
// Zwracanie standardowego Response
return json({ products });
}
// Action obsługujący formularz - również funkcja serwerowa
export async function action({ request }: ActionFunctionArgs) {
// FormData z natywnego Web API
const formData = await request.formData();
const name = formData.get("name");
// Walidacja i zapisanie danych
const product = await db.product.create({
data: { name: String(name) }
});
// Przekierowanie po udanej operacji
return redirect(`/products/${product.id}`);
}
// Komponent - działa bez JavaScript dzięki progressive enhancement
export default function ProductsRoute() {
const { products } = useLoaderData<typeof loader>();
return (
<div>
<Form method="post">
<input name="name" required />
<button type="submit">Dodaj produkt</button>
</Form>
<ul>
{products.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</div>
);
}
Diagram:
flowchart TD
A[Żądanie użytkownika] --> B[Remix Server]
B --> C{Typ żądania}
C -->|GET| D[Wywołanie loaderów]
C -->|POST/PUT/DELETE| E[Wywołanie action]
D --> F[Równoległe pobieranie danych]
E --> G[Mutacja danych]
F --> H[Renderowanie HTML na serwerze]
G --> I[Rewalidacja danych]
I --> H
H --> J[Wysłanie HTML do klienta]
J --> K[Hydratacja JavaScript]
K --> L[Progressive Enhancement]
Materiały:
- Remix Philosophy - Official Docs
- Remix vs Next.js - Kent C. Dodds
- Introduction to Remix - Remix Docs
2. Jakie są kluczowe różnice między Remix a Next.js?
Odpowiedź w 30 sekund: Główne różnice to: Remix domyślnie renderuje wszystko na serwerze i wykorzystuje progressive enhancement, podczas gdy Next.js oferuje wybór między SSR, SSG i CSR. Remix opiera się na Web Standards (FormData, Request/Response), Next.js ma własne abstrakcje. Remix nie ma build-time rendering (wszystko runtime), a Next.js promuje Static Generation jako domyślną metodę.
Odpowiedź w 2 minuty: Remix i Next.js to dwa najpopularniejsze full-stack frameworki React, ale różnią się fundamentalnie w filozofii i podejściu. Next.js oferuje elastyczność wyboru między różnymi strategiami renderowania (SSG, SSR, ISR, CSR) i promuje statyczną generację jako domyślną opcję dla lepszej wydajności. Remix natomiast renderuje wszystko w runtime na serwerze, nie ma koncepcji build-time generation.
W kwestii obsługi danych i formularzy różnice są znaczące. Next.js wykorzystuje własne abstrakcje jak API Routes, Server Components i Server Actions. Remix opiera się na standardowych Web API - używa natywnych Request/Response objects, FormData API i standardowych form submissions. To oznacza, że w Remix formularze działają bez JavaScript, podczas gdy Next.js tradycyjnie wymaga JavaScript do obsługi interakcji.
Routing również się różni: oba frameworki używają file-based routing, ale Remix ma bardziej zaawansowane nested routing z automatic data loading i error boundaries na każdym poziomie zagnieżdżenia. Next.js App Router wprowadził podobne koncepcje, ale Remix miał je od początku jako core feature.
Kolejna kluczowa różnica to podejście do optimistic UI i rewalidacji danych. Remix ma wbudowany system automatycznej rewalidacji po każdej mutacji, podczas gdy w Next.js trzeba to często implementować ręcznie lub używać dodatkowych bibliotek. Remix również nie wymaga osobnego API layer - loaders i actions są naturalną warstwą API.
Przykład kodu:
// REMIX - formularz z wykorzystaniem Web Standards
export async function action({ request }: ActionFunctionArgs) {
// Natywne FormData API
const formData = await request.formData();
const intent = formData.get("intent");
if (intent === "delete") {
await deleteProduct(formData.get("id"));
} else {
await createProduct({
name: formData.get("name"),
price: Number(formData.get("price"))
});
}
// Automatyczna rewalidacja po przekierowaniu
return redirect("/products");
}
export default function ProductForm() {
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
return (
// Działa bez JavaScript!
<Form method="post">
<input name="name" required />
<input name="price" type="number" required />
<button name="intent" value="create" disabled={isSubmitting}>
{isSubmitting ? "Zapisywanie..." : "Zapisz"}
</button>
</Form>
);
}
// NEXT.JS - podobna funkcjonalność z Server Actions
"use server"
export async function createProduct(formData: FormData) {
// Własna abstrakcja Next.js
const name = formData.get("name");
const price = formData.get("price");
await db.product.create({
data: { name, price: Number(price) }
});
// Manualna rewalidacja cache
revalidatePath("/products");
redirect("/products");
}
// Komponent wymaga "use client" dla interakcji
"use client"
export default function ProductForm() {
const [pending, setPending] = useState(false);
return (
<form action={createProduct} onSubmit={() => setPending(true)}>
<input name="name" required />
<input name="price" type="number" required />
<button disabled={pending}>
{pending ? "Zapisywanie..." : "Zapisz"}
</button>
</form>
);
}
// NESTED ROUTING - Remix
// app/routes/dashboard.tsx (parent route)
export async function loader() {
return json({ user: await getUser() });
}
export default function Dashboard() {
const { user } = useLoaderData<typeof loader>();
return (
<div>
<h1>Witaj {user.name}</h1>
{/* Outlet renderuje zagnieżdżone trasy */}
<Outlet />
</div>
);
}
// app/routes/dashboard.products.tsx (child route)
export async function loader() {
// Ładowane równolegle z parent loader!
return json({ products: await getProducts() });
}
Diagram:
graph TB
subgraph "Remix"
A1[Żądanie] --> B1[Runtime SSR]
B1 --> C1[Równoległe loadery]
C1 --> D1[HTML + Progressive Enhancement]
D1 --> E1[Natywne Web API]
end
subgraph "Next.js"
A2[Żądanie] --> B2{Strategia renderowania}
B2 -->|SSG| C2[Build-time HTML]
B2 -->|SSR| D2[Runtime SSR]
B2 -->|ISR| E2[Regeneracja w tle]
C2 --> F2[Własne abstrakcje API]
D2 --> F2
E2 --> F2
end
Materiały:
- Remix vs Next.js comparison - Remix Blog
- Progressive Enhancement in Remix - Ryan Florence
- Next.js App Router vs Remix - LogRocket Blog
3. Jak Remix wykorzystuje standardy Web API (Request, Response, FormData)?
Odpowiedź w 30 sekund: Remix bazuje na Web Fetch API - loadery i actions przyjmują standardowe Request objects i zwracają Response objects. FormData API jest używany do obsługi formularzy zamiast kontrolowanych komponentów React. To podejście eliminuje framework-specific abstrakcje, sprawia że kod jest bardziej portable i działa zgodnie z natywnym zachowaniem przeglądarki.
Odpowiedź w 2 minuty: Remix przyjmuje filozofię "use the platform", co oznacza maksymalne wykorzystanie natywnych Web Standards zamiast tworzenia własnych abstrakcji. W centrum tego podejścia znajdują się trzy kluczowe API: Request, Response i FormData.
Request object jest przekazywany do każdego loadera i action - to standardowy obiekt Fetch API zawierający wszystkie informacje o żądaniu HTTP (metoda, nagłówki, URL, body). Developer może używać wszystkich standardowych metod jak request.headers.get(), request.formData(), czy new URL(request.url). To oznacza, że umiejętności nabyte w Remix są bezpośrednio transferowalne do innych kontekstów webowych, nie tylko React.
Response object jest używany do zwracania danych z loaderów i actions. Remix oferuje helper functions jak json(), redirect() czy defer(), ale pod spodem to standardowe Response objects. Developer może ustawiać własne status codes, headers czy cookies używając standardowych Web API.
FormData API jest sercem obsługi formularzy w Remix. Zamiast kontrolowanych inputów z React state, Remix używa niekontrolowanych formularzy z natywnym zachowaniem przeglądarki. Po submicie, dane są automatycznie serializowane do FormData i wysyłane do action. To sprawia, że formularze działają nawet bez JavaScript, co jest fundamentem progressive enhancement.
Przykład kodu:
// Wykorzystanie Request API w loaderze
export async function loader({ request }: LoaderFunctionArgs) {
// Parsowanie URL i parametrów zapytania
const url = new URL(request.url);
const page = Number(url.searchParams.get("page")) || 1;
const sort = url.searchParams.get("sort") || "name";
// Odczyt nagłówków (np. cookies)
const cookieHeader = request.headers.get("Cookie");
const cookie = await parseCookie(cookieHeader);
// Sprawdzenie metody HTTP
if (request.method !== "GET") {
throw new Response("Method not allowed", { status: 405 });
}
const data = await fetchData({ page, sort, userId: cookie.userId });
// Zwracanie Response z custom headers
return json(data, {
headers: {
"Cache-Control": "max-age=300, s-maxage=3600",
"X-Custom-Header": "wartość"
}
});
}
// Wykorzystanie FormData w action
export async function action({ request }: ActionFunctionArgs) {
// Pobranie FormData ze standardowego Request
const formData = await request.formData();
// Odczyt wartości z różnych typów inputów
const email = formData.get("email"); // text input
const age = Number(formData.get("age")); // number input
const terms = formData.get("terms") === "on"; // checkbox
const role = formData.get("role"); // select/radio
// Obsługa multiple values (np. checkboxes)
const interests = formData.getAll("interests");
// Obsługa file uploads
const avatar = formData.get("avatar") as File;
if (avatar && avatar.size > 0) {
const buffer = await avatar.arrayBuffer();
await uploadFile(buffer, avatar.name);
}
// Walidacja
const errors: Record<string, string> = {};
if (!email?.includes("@")) {
errors.email = "Nieprawidłowy email";
}
if (Object.keys(errors).length > 0) {
// Zwracanie błędów walidacji jako Response
return json({ errors }, { status: 400 });
}
// Zapisanie danych
await createUser({ email, age, terms, role, interests });
// Redirect z custom header (np. flash message)
return redirect("/success", {
headers: {
"Set-Cookie": await createFlashCookie("Użytkownik utworzony!")
}
});
}
// Komponent wykorzystujący standardowy HTML form
export default function SignupForm() {
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
return (
// Natywny HTML form - działa bez JavaScript
<Form method="post" encType="multipart/form-data">
<div>
<input
name="email"
type="email"
required
aria-invalid={!!actionData?.errors?.email}
/>
{actionData?.errors?.email && (
<span className="error">{actionData.errors.email}</span>
)}
</div>
<input name="age" type="number" min="0" />
<label>
<input name="terms" type="checkbox" required />
Akceptuję regulamin
</label>
<select name="role">
<option value="user">Użytkownik</option>
<option value="admin">Administrator</option>
</select>
{/* Multiple checkboxes z tą samą nazwą */}
<fieldset>
<legend>Zainteresowania:</legend>
<label><input name="interests" type="checkbox" value="coding" /> Programowanie</label>
<label><input name="interests" type="checkbox" value="design" /> Design</label>
<label><input name="interests" type="checkbox" value="music" /> Muzyka</label>
</fieldset>
<input name="avatar" type="file" accept="image/*" />
<button type="submit" disabled={navigation.state === "submitting"}>
{navigation.state === "submitting" ? "Wysyłanie..." : "Zapisz"}
</button>
</Form>
);
}
// Zaawansowane użycie - streaming Response z defer()
export async function loader() {
// Dane krytyczne - czekamy na nie
const criticalData = await fetchCriticalData();
// Dane niekrytyczne - nie czekamy, streamujemy
const slowDataPromise = fetchSlowData();
return defer({
criticalData,
slowData: slowDataPromise // Promise, nie await!
});
}
export default function StreamingRoute() {
const data = useLoaderData<typeof loader>();
return (
<div>
{/* Renderowane natychmiast */}
<h1>{data.criticalData.title}</h1>
{/* Renderowane gdy Promise się resolve */}
<Suspense fallback={<div>Ładowanie...</div>}>
<Await resolve={data.slowData}>
{(slowData) => <div>{slowData.content}</div>}
</Await>
</Suspense>
</div>
);
}
Diagram:
sequenceDiagram
participant B as Przeglądarka
participant R as Remix Server
participant L as Loader/Action
participant DB as Baza Danych
B->>R: POST /signup (FormData)
R->>L: action({ request })
L->>L: await request.formData()
L->>L: Walidacja danych
L->>DB: Zapis w bazie
DB-->>L: Potwierdzenie
L->>R: Response (redirect/json)
R->>B: HTTP Response (302/200)
Note over B,DB: Wszystko oparte na Web Standards
Note over L: Request, Response, FormData
Note over B: Działa nawet bez JavaScript!
Materiały:
↑ Powrót na górę4. Czym jest progressive enhancement w kontekście Remix?
Odpowiedź w 30 sekund: Progressive enhancement w Remix oznacza, że aplikacja najpierw działa jako tradycyjna strona WWW z formularzami HTML i linkami, a następnie JavaScript stopniowo wzbogaca doświadczenie użytkownika o SPA-like interactions. Podstawowa funkcjonalność (formularze, nawigacja, wyświetlanie danych) działa bez JavaScript, a gdy JavaScript się załaduje, dodawane są optimistic UI, transitions i lepsze UX.
Odpowiedź w 2 minuty: Progressive enhancement to fundamentalna filozofia Remix, która odwraca tradycyjne podejście do budowania aplikacji React. Zamiast zaczynać od JavaScript i próbować dodać server-side rendering jako optymalizację, Remix zaczyna od w pełni funkcjonalnej aplikacji serwerowej, która następnie jest wzbogacana przez JavaScript.
W praktyce oznacza to, że każdy formularz w Remix działa jako standardowy HTML form z method="post" i action URL. Gdy użytkownik submituje formularz bez JavaScript, przeglądarka wykonuje standardowe HTTP POST request, serwer przetwarza dane w action, a następnie odsyła nową stronę. To jest baseline functionality, która zawsze działa.
Gdy JavaScript się załaduje, Remix automatycznie "przechwytuje" submit events i przekształca je w fetch requests. Użytkownik dostaje wtedy SPA experience - brak pełnego przeładowania strony, zachowanie scroll position, możliwość optimistic UI. Ale to wszystko jest enhancement - dodatkiem do działającej podstawy.
To podejście ma ogromne zalety: lepszy UX w wolnych sieciach (HTML działa zanim JavaScript się załaduje), lepsza dostępność (screen readers i inne assistive technologies lepiej radzą sobie ze standardowym HTML), większa odporność (jeśli JavaScript się nie załaduje lub wystąpi błąd JS, aplikacja nadal działa), i lepsze SEO (crawlers widzą w pełni funkcjonalną stronę).
Remix oferuje też hooks jak useNavigation(), useFetcher() i useTransition() do budowania zaawansowanych pending states, optimistic UI i loading indicators - ale to wszystko są enhancements na działającej podstawie.
Przykład kodu:
// Action - obsługuje zarówno JS jak i no-JS requests
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const title = formData.get("title");
const content = formData.get("content");
// Walidacja
const errors: Record<string, string> = {};
if (!title) errors.title = "Tytuł jest wymagany";
if (!content) errors.content = "Treść jest wymagana";
if (Object.keys(errors).length > 0) {
return json({ errors }, { status: 400 });
}
const post = await db.post.create({
data: { title: String(title), content: String(content) }
});
return redirect(`/posts/${post.id}`);
}
export default function NewPost() {
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
// Stan submitu - enhancement gdy JS jest włączony
const isSubmitting = navigation.state === "submitting";
const isSuccessful = navigation.state === "loading" &&
navigation.formData != null;
return (
<div>
{/* BASELINE: Zwykły HTML form - działa bez JavaScript */}
<Form method="post">
<div>
<label htmlFor="title">Tytuł:</label>
<input
id="title"
name="title"
type="text"
required
aria-invalid={!!actionData?.errors?.title}
aria-describedby={actionData?.errors?.title ? "title-error" : undefined}
/>
{/* Błędy walidacji - działają z i bez JS */}
{actionData?.errors?.title && (
<span id="title-error" className="error">
{actionData.errors.title}
</span>
)}
</div>
<div>
<label htmlFor="content">Treść:</label>
<textarea
id="content"
name="content"
required
aria-invalid={!!actionData?.errors?.content}
/>
{actionData?.errors?.content && (
<span className="error">{actionData.errors.content}</span>
)}
</div>
{/* BASELINE: Zwykły submit button */}
<button type="submit">
Opublikuj
</button>
{/* ENHANCEMENT: Pending state gdy JS działa */}
{isSubmitting && (
<div className="spinner" aria-live="polite">
Zapisywanie...
</div>
)}
{/* ENHANCEMENT: Success feedback */}
{isSuccessful && (
<div className="success" aria-live="polite">
Zapisano! Przekierowywanie...
</div>
)}
</Form>
</div>
);
}
// Zaawansowany przykład: Optimistic UI jako enhancement
export default function TodoList() {
const { todos } = useLoaderData<typeof loader>();
const fetcher = useFetcher();
// ENHANCEMENT: Optimistic UI - pokazuj zmianę natychmiast
const optimisticTodos = todos.map(todo => {
if (fetcher.formData?.get("todoId") === todo.id) {
// Symuluj zmianę przed odpowiedzią serwera
return {
...todo,
completed: fetcher.formData.get("completed") === "true"
};
}
return todo;
});
return (
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id}>
{/* BASELINE: Formularz który działa bez JS */}
<fetcher.Form method="post" action="/toggle-todo">
<input type="hidden" name="todoId" value={todo.id} />
<input
type="hidden"
name="completed"
value={String(!todo.completed)}
/>
<button type="submit">
{/* BASELINE: Pokazuje aktualny stan */}
{/* ENHANCEMENT: Pokazuje optimistic state */}
{todo.completed ? "✓" : "○"} {todo.title}
</button>
{/* ENHANCEMENT: Loading indicator */}
{fetcher.state === "submitting" && (
<span className="pending">...</span>
)}
</fetcher.Form>
</li>
))}
</ul>
);
}
// Przykład nawigacji z progressive enhancement
export function Navigation() {
const navigation = useNavigation();
return (
<nav>
{/* BASELINE: Zwykłe linki - działają bez JS */}
<Link to="/home">Home</Link>
<Link to="/about">O nas</Link>
<Link to="/contact">Kontakt</Link>
{/* ENHANCEMENT: Global loading indicator */}
{navigation.state === "loading" && (
<div className="global-spinner" aria-live="polite">
Ładowanie strony...
</div>
)}
</nav>
);
}
// Przykład wyszukiwania z debouncing jako enhancement
export default function SearchPage() {
const [searchParams] = useSearchParams();
const { results } = useLoaderData<typeof loader>();
const submit = useSubmit();
return (
<div>
{/* BASELINE: Formularz GET z instant search przez przeładowanie */}
<Form method="get">
<input
name="q"
type="search"
defaultValue={searchParams.get("q") ?? ""}
onChange={(e) => {
// ENHANCEMENT: Debounced submit gdy JS działa
const isFirstSearch = searchParams.get("q") === null;
submit(e.currentTarget.form, {
replace: !isFirstSearch, // Nie zapychaj historii
});
}}
/>
{/* BASELINE: Submit button dla no-JS */}
<button type="submit">Szukaj</button>
</Form>
<ul>
{results.map(result => (
<li key={result.id}>{result.name}</li>
))}
</ul>
</div>
);
}
Diagram:
flowchart TD
A[Użytkownik otwiera stronę] --> B[Serwer wysyła HTML]
B --> C{JavaScript załadowany?}
C -->|NIE| D[BASELINE: Zwykła strona HTML]
D --> E[Formularze = pełne przeładowanie]
D --> F[Linki = pełne przeładowanie]
D --> G[Brak loading states]
C -->|TAK| H[ENHANCEMENT: SPA Experience]
H --> I[Formularze = fetch requests]
H --> J[Linki = client-side routing]
H --> K[Optimistic UI]
H --> L[Loading indicators]
H --> M[Transitions]
E --> N[Aplikacja działa!]
F --> N
G --> N
I --> N
J --> N
K --> N
L --> N
M --> N
style D fill:#90EE90
style E fill:#90EE90
style F fill:#90EE90
style G fill:#90EE90
style H fill:#87CEEB
style I fill:#87CEEB
style J fill:#87CEEB
style K fill:#87CEEB
style L fill:#87CEEB
style M fill:#87CEEB
Materiały:
- Progressive Enhancement - Remix Docs
- Building Resilient Web Apps - Ryan Florence
- Progressive Enhancement Explained - MDN
5. Jak działa architektura nested routing w Remix?
Odpowiedź w 30 sekund:
Nested routing w Remix pozwala tworzyć hierarchię tras, gdzie każdy poziom ma własny loader, action, error boundary i component. Parent routes renderują <Outlet /> dla child routes, a wszystkie loadery w hierarchii są wywoływane równolegle. To umożliwia izolację danych, błędów i UI na każdym poziomie zagnieżdżenia, eliminując "data waterfalls" i upraszając zarządzanie stanem.
Odpowiedź w 2 minuty: Nested routing jest jedną z najważniejszych feature Remix, zapożyczoną z React Router. Architektura pozwala na tworzenie hierarchii tras, gdzie każda trasa może mieć swoje własne dane, błędy i UI, które są kompozytowane razem w finalnej stronie.
Fundamentalna różnica w porównaniu do flat routing polega na tym, że parent route renderuje <Outlet /> component, który jest placeholderem dla child routes. Gdy użytkownik nawiguje do /dashboard/settings, Remix renderuje hierarchię: layout root -> dashboard route -> settings route. Każdy poziom może mieć własny loader do pobierania danych, własny action do obsługi mutacji, oraz własny error boundary do obsługi błędów.
Kluczową zaletą jest równoległe ładowanie danych - wszystkie loadery w hierarchii są wywoływane jednocześnie, nie sekwencyjnie. W tradycyjnym podejściu parent component musiałby się wyrenderować, pobrać dane, a dopiero potem child component mógłby zacząć pobierać swoje dane (data waterfall). Remix eliminuje ten problem wywołując wszystkie loadery równolegle w momencie matchowania route.
Nested routing pozwala też na partial revalidation - gdy wykonasz action na child route, Remix automatycznie rewaliduje dane tylko w tej trasie i jej parent routes, nie w całej aplikacji. Error boundaries działają podobnie - błąd w child route nie crashuje całej aplikacji, tylko pokazuje error UI w miejscu tego route, podczas gdy reszta strony działa normalnie.
To podejście promuje lepszą separację odpowiedzialności - każdy route jest odpowiedzialny tylko za swoje dane i UI, co ułatwia reasoning o kodzie, testowanie i utrzymanie aplikacji.
Przykład kodu:
// app/root.tsx - Root layout
export async function loader() {
// Dane globalne dostępne w całej aplikacji
return json({
user: await getUser(),
env: process.env.PUBLIC_ENV
});
}
export default function Root() {
const { user } = useLoaderData<typeof loader>();
return (
<html lang="pl">
<head>
<Meta />
<Links />
</head>
<body>
<header>
<nav>
<Link to="/">Home</Link>
<Link to="/dashboard">Dashboard</Link>
{user ? <span>{user.name}</span> : <Link to="/login">Login</Link>}
</nav>
</header>
{/* Outlet renderuje matched child routes */}
<main>
<Outlet />
</main>
<footer>© 2025</footer>
<Scripts />
</body>
</html>
);
}
// app/routes/dashboard.tsx - Parent route z layoutem
export async function loader({ request }: LoaderFunctionArgs) {
// Wymuszenie autoryzacji
const user = await requireAuth(request);
// Dane współdzielone przez wszystkie child routes dashboard
return json({
user,
stats: await getUserStats(user.id)
});
}
export default function Dashboard() {
const { user, stats } = useLoaderData<typeof loader>();
return (
<div className="dashboard">
<aside>
<h2>Dashboard - {user.name}</h2>
<nav>
{/* Nested navigation */}
<NavLink to="/dashboard">Przegląd</NavLink>
<NavLink to="/dashboard/settings">Ustawienia</NavLink>
<NavLink to="/dashboard/projects">Projekty</NavLink>
<NavLink to="/dashboard/billing">Płatności</NavLink>
</nav>
<div className="stats">
<p>Projekty: {stats.projectCount}</p>
<p>Członkostwo: {stats.membershipLevel}</p>
</div>
</aside>
<div className="content">
{/* Child routes renderują się tutaj */}
<Outlet />
</div>
</div>
);
}
// Error boundary dla całego dashboardu
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error) && error.status === 401) {
return (
<div>
<h1>Brak autoryzacji</h1>
<Link to="/login">Zaloguj się</Link>
</div>
);
}
return <div>Wystąpił błąd w dashboardzie</div>;
}
// app/routes/dashboard._index.tsx - Default child route (przegląd)
export async function loader({ request }: LoaderFunctionArgs) {
const user = await requireAuth(request);
// Loader wywołany równolegle z parent loader!
return json({
recentActivity: await getRecentActivity(user.id),
notifications: await getNotifications(user.id)
});
}
export default function DashboardIndex() {
const { recentActivity, notifications } = useLoaderData<typeof loader>();
// Dostęp do danych z parent route przez useMatches()
const matches = useMatches();
const dashboardData = matches.find(m => m.id === "routes/dashboard")?.data;
return (
<div>
<h1>Przegląd</h1>
<section>
<h2>Powiadomienia ({notifications.length})</h2>
<ul>
{notifications.map(n => (
<li key={n.id}>{n.message}</li>
))}
</ul>
</section>
<section>
<h2>Ostatnia aktywność</h2>
<ul>
{recentActivity.map(a => (
<li key={a.id}>{a.description}</li>
))}
</ul>
</section>
</div>
);
}
// app/routes/dashboard.settings.tsx - Child route z własnym action
export async function loader({ request }: LoaderFunctionArgs) {
const user = await requireAuth(request);
return json({
settings: await getUserSettings(user.id)
});
}
export async function action({ request }: ActionFunctionArgs) {
const user = await requireAuth(request);
const formData = await request.formData();
await updateUserSettings(user.id, {
theme: formData.get("theme"),
notifications: formData.get("notifications") === "on"
});
// Po action Remix automatycznie rewaliduje:
// - dashboard.settings loader (ta trasa)
// - dashboard loader (parent)
// - root loader (jeśli potrzebne)
return json({ success: true });
}
export default function Settings() {
const { settings } = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
return (
<div>
<h1>Ustawienia</h1>
{actionData?.success && (
<div className="alert-success">Zapisano zmiany!</div>
)}
<Form method="post">
<label>
Motyw:
<select name="theme" defaultValue={settings.theme}>
<option value="light">Jasny</option>
<option value="dark">Ciemny</option>
</select>
</label>
<label>
<input
type="checkbox"
name="notifications"
defaultChecked={settings.notifications}
/>
Powiadomienia email
</label>
<button type="submit" disabled={navigation.state === "submitting"}>
{navigation.state === "submitting" ? "Zapisywanie..." : "Zapisz"}
</button>
</Form>
</div>
);
}
// Własny error boundary tylko dla settings
export function ErrorBoundary() {
return (
<div className="error">
<h2>Nie można załadować ustawień</h2>
<p>Spróbuj ponownie później</p>
</div>
);
}
// app/routes/dashboard.projects.$projectId.tsx - Dynamic nested route
export async function loader({ params }: LoaderFunctionArgs) {
const project = await getProject(params.projectId!);
if (!project) {
throw new Response("Nie znaleziono projektu", { status: 404 });
}
return json({ project });
}
export default function ProjectDetails() {
const { project } = useLoaderData<typeof loader>();
return (
<div>
<h1>{project.name}</h1>
<p>{project.description}</p>
{/* Możliwość dalszego zagnieżdżania! */}
<Outlet />
</div>
);
}
// Przykład użycia useMatches() do dostępu do danych z parent routes
export function Breadcrumbs() {
const matches = useMatches();
return (
<nav aria-label="breadcrumb">
<ol>
{matches
.filter(match => match.handle?.breadcrumb)
.map((match, index) => (
<li key={index}>
<Link to={match.pathname}>
{match.handle.breadcrumb(match.data)}
</Link>
</li>
))}
</ol>
</nav>
);
}
// Dodanie breadcrumb handle w route
export const handle = {
breadcrumb: (data: any) => data.project.name
};
Diagram:
flowchart TD
subgraph "Struktura plików"
A[app/root.tsx] --> B[app/routes/dashboard.tsx]
B --> C[app/routes/dashboard._index.tsx]
B --> D[app/routes/dashboard.settings.tsx]
B --> E[app/routes/dashboard.projects.tsx]
E --> F[app/routes/dashboard.projects.$projectId.tsx]
end
subgraph "Renderowanie /dashboard/projects/123"
G[Root Layout] --> H[Dashboard Layout]
H --> I[Project Details]
end
subgraph "Ładowanie danych równoległe"
J[Request: /dashboard/projects/123] --> K[Match Routes]
K --> L1[root.loader]
K --> L2[dashboard.loader]
K --> L3[projects.$projectId.loader]
L1 --> M[Combine Data]
L2 --> M
L3 --> M
M --> N[Render HTML]
end
style L1 fill:#87CEEB
style L2 fill:#87CEEB
style L3 fill:#87CEEB
style M fill:#90EE90
Diagram przepływu błędów:
flowchart TD
A[Błąd w Child Route] --> B{Error Boundary w Child?}
B -->|TAK| C[Renderuj Error UI w Child]
B -->|NIE| D{Error Boundary w Parent?}
D -->|TAK| E[Renderuj Error UI w Parent]
D -->|NIE| F{Error Boundary w Root?}
F -->|TAK| G[Renderuj Error UI w Root]
F -->|NIE| H[Default Error Page]
C --> I[Reszta strony działa normalnie]
E --> I
style C fill:#90EE90
style E fill:#FFD700
style G fill:#FFA500
style H fill:#FF6347
Materiały:
↑ Powrót na góręRemix - Sekcja 2: Routing
11. Czym jest plik root.tsx i jaką pełni rolę?
Odpowiedź w 30 sekund:
Plik root.tsx to główny route aplikacji Remix, który opakowuje wszystkie pozostałe route'y. Zawiera strukturę HTML dokumentu (<html>, <head>, <body>), globalne style i skrypty, oraz renderuje wszystkie zagnieżdżone route'y przez komponent <Outlet />. Jest punktem wejścia dla całej aplikacji.
Odpowiedź w 2 minuty:
Plik app/root.tsx jest specjalnym route'em w Remix, który stanowi korzeń całej aplikacji. Jest to jedyny plik, który renderuje pełną strukturę HTML dokumentu, włączając tagi <html>, <head> i <body>. Wszystkie inne route'y są renderowane wewnątrz tego root route'a poprzez komponent <Outlet />.
W root.tsx definiujesz globalną strukturę aplikacji, metatagi, linki do stylów, skrypty oraz inne zasoby potrzebne na każdej stronie. Remix dostarcza specjalne komponenty jak <Meta />, <Links />, <ScrollRestoration /> i <Scripts />, które automatycznie wstrzykują odpowiednie tagi w odpowiednich miejscach. Możesz też dodać globalny ErrorBoundary i loader dla danych dostępnych w całej aplikacji (np. informacje o zalogowanym użytkowniku).
Root.tsx to również miejsce, gdzie możesz zaimplementować globalne UI elementy jak powiadomienia toast, modale, czy globalne loading indicators. Dzięki temu, że jest to route jak każdy inny, możesz używać wszystkich funkcji Remix jak loader, action, ErrorBoundary czy CatchBoundary. Jest to foundation, na którym budowana jest reszta aplikacji.
Przykład kodu:
// app/root.tsx
import type { LinksFunction, LoaderFunctionArgs } from "@remix-run/node";
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useLoaderData,
useRouteError,
isRouteErrorResponse,
} from "@remix-run/react";
import { json } from "@remix-run/node";
import stylesheet from "~/styles/global.css";
// Definiowanie globalnych linków do stylów
export const links: LinksFunction = () => [
{ rel: "stylesheet", href: stylesheet },
{ rel: "icon", href: "/favicon.ico" },
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
];
// Loader dla globalnych danych
export async function loader({ request }: LoaderFunctionArgs) {
const user = await getOptionalUser(request);
const env = {
NODE_ENV: process.env.NODE_ENV,
PUBLIC_API_URL: process.env.PUBLIC_API_URL,
};
return json({ user, env });
}
// Główny komponent aplikacji
export default function App() {
const { user, env } = useLoaderData<typeof loader>();
return (
<html lang="pl">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
{/* Meta tagi z wszystkich route'ów */}
<Meta />
{/* Linki do stylów z wszystkich route'ów */}
<Links />
</head>
<body>
{/* Globalny header */}
<header>
<nav>
<a href="/">Strona główna</a>
{user ? (
<>
<a href="/dashboard">Dashboard</a>
<span>Zalogowany jako: {user.name}</span>
</>
) : (
<a href="/login">Zaloguj się</a>
)}
</nav>
</header>
{/* Tutaj renderują się wszystkie route'y */}
<Outlet />
{/* Globalny footer */}
<footer>
<p>© 2024 Moja Aplikacja</p>
</footer>
{/* Przywracanie pozycji scrollu przy nawigacji */}
<ScrollRestoration />
{/* Skrypty z wszystkich route'ów */}
<Scripts />
{/* Hot reload w development */}
<LiveReload />
{/* Przekazanie zmiennych środowiskowych do klienta */}
<script
dangerouslySetInnerHTML={{
__html: `window.ENV = ${JSON.stringify(env)}`,
}}
/>
</body>
</html>
);
}
// Globalny ErrorBoundary
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<html lang="pl">
<head>
<title>Błąd {error.status}</title>
<Meta />
<Links />
</head>
<body>
<h1>
{error.status} {error.statusText}
</h1>
<p>{error.data}</p>
<Scripts />
</body>
</html>
);
}
return (
<html lang="pl">
<head>
<title>Wystąpił błąd</title>
<Meta />
<Links />
</head>
<body>
<h1>Wystąpił nieoczekiwany błąd</h1>
<p>Przepraszamy za niedogodności.</p>
<Scripts />
</body>
</html>
);
}
Diagram:
flowchart TD
A[root.tsx] --> B[html element]
B --> C[head]
B --> D[body]
C --> E[Meta - metatagi]
C --> F[Links - style CSS]
D --> G[Global Header/Nav]
D --> H[Outlet - route'y potomne]
D --> I[Global Footer]
D --> J[ScrollRestoration]
D --> K[Scripts - JS]
D --> L[LiveReload dev]
H --> M[dashboard.tsx]
H --> N[blog.tsx]
H --> O[about.tsx]
M --> P[dashboard._index.tsx]
M --> Q[dashboard.settings.tsx]
style A fill:#ff6b6b
style H fill:#4ecdc4
style M fill:#ffe66d
style N fill:#ffe66d
style O fill:#ffe66d
Materiały
↑ Powrót na górę6. Jak działa system routingu oparty na plikach w Remix?
Odpowiedź w 30 sekund:
Remix wykorzystuje system routingu oparty na strukturze katalogów w folderze app/routes/. Każdy plik w tym katalogu automatycznie tworzy odpowiadającą mu ścieżkę URL. Nazwa pliku determinuje ścieżkę, a zagnieżdżenie katalogów pozwala na tworzenie hierarchicznych layoutów.
Odpowiedź w 2 minuty:
System routingu w Remix opiera się na konwencji "file-based routing", gdzie struktura plików bezpośrednio odzwierciedla strukturę URL aplikacji. Pliki umieszczone w katalogu app/routes/ są automatycznie mapowane na ścieżki w aplikacji. Na przykład, plik app/routes/about.tsx staje się dostępny pod adresem /about.
Remix wspiera różne konwencje nazewnictwa plików. Możesz używać notacji z kropkami (np. posts.new.tsx dla /posts/new) lub zagnieżdżonych katalogów (np. posts/new.tsx dla tej samej ścieżki). System automatycznie obsługuje pliki specjalne jak _index.tsx (route dla ścieżki nadrzędnej) oraz _layout.tsx (layout bez wpływu na URL).
Kluczową zaletą tego podejścia jest automatyczna optymalizacja ładowania kodu - Remix wie dokładnie, które komponenty i dane są potrzebne dla danej ścieżki i ładuje tylko niezbędne zasoby. Zagnieżdżone route'y automatycznie dziedziczą dane i błędy z route'ów nadrzędnych, co pozwala na eleganckie zarządzanie stanem i obsługą błędów w całej hierarchii.
Przykład kodu:
// Struktura katalogów i odpowiadające im ścieżki:
// app/routes/_index.tsx -> "/"
export default function Index() {
return <h1>Strona główna</h1>;
}
// app/routes/about.tsx -> "/about"
export default function About() {
return <h1>O nas</h1>;
}
// app/routes/blog._index.tsx -> "/blog"
export default function BlogIndex() {
return <h1>Lista postów</h1>;
}
// app/routes/blog.$slug.tsx -> "/blog/:slug"
export default function BlogPost() {
return <h1>Pojedynczy post</h1>;
}
// Alternatywnie z zagnieżdżonymi katalogami:
// app/routes/blog/index.tsx -> "/blog"
// app/routes/blog/$slug.tsx -> "/blog/:slug"
Diagram:
flowchart TD
A[app/routes/] --> B[_index.tsx<br/>/]
A --> C[about.tsx<br/>/about]
A --> D[blog._index.tsx<br/>/blog]
A --> E[blog.$slug.tsx<br/>/blog/:slug]
A --> F[products/]
F --> G[_index.tsx<br/>/products]
F --> H[$id.tsx<br/>/products/:id]
style A fill:#e1f5ff
style B fill:#fff4e6
style C fill:#fff4e6
style D fill:#fff4e6
style E fill:#ffe6e6
style F fill:#e1f5ff
style G fill:#fff4e6
style H fill:#ffe6e6
Materiały
↑ Powrót na górę7. Czym są segmenty dynamiczne ($param) i jak je definiować?
Odpowiedź w 30 sekund:
Segmenty dynamiczne w Remix to parametry URL oznaczane prefiksem $ w nazwie pliku. Plik app/routes/users.$id.tsx będzie obsługiwał ścieżki jak /users/123 lub /users/abc, gdzie wartość $id jest dostępna przez params.id w loaderze i komponencie.
Odpowiedź w 2 minuty:
Segmenty dynamiczne pozwalają na tworzenie route'ów, które obsługują zmienne wartości w URL. W Remix definiujemy je poprzez dodanie prefiksu $ przed nazwą parametru w nazwie pliku. Na przykład $userId.tsx stworzy segment dynamiczny, który może pasować do dowolnej wartości w tym miejscu URL.
Wartości segmentów dynamicznych są dostępne poprzez obiekt params zarówno w funkcji loader jak i w komponencie (przez hook useParams()). Możesz mieć wiele segmentów dynamicznych w jednej ścieżce, np. users.$userId.posts.$postId.tsx dla /users/123/posts/456. Remix automatycznie parsuje te wartości i udostępnia je jako właściwości obiektu params.
Ważne jest, że segmenty dynamiczne mają niższy priorytet niż statyczne - jeśli masz zarówno users.me.tsx jak i users.$id.tsx, to dla URL /users/me zostanie użyty statyczny route. To pozwala na tworzenie wyjątków dla specjalnych przypadków. Segmenty dynamiczne są podstawowym narzędziem do budowania aplikacji z dynamiczną treścią, jak blogi, sklepy internetowe czy panele administracyjne.
Przykład kodu:
// app/routes/users.$userId.tsx
import { json, LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData, useParams } from "@remix-run/react";
// Loader ma dostęp do parametrów
export async function loader({ params }: LoaderFunctionArgs) {
const userId = params.userId; // wartość z URL
const user = await getUserById(userId);
if (!user) {
throw new Response("Nie znaleziono użytkownika", { status: 404 });
}
return json({ user });
}
export default function UserProfile() {
const { user } = useLoaderData<typeof loader>();
const params = useParams(); // alternatywny dostęp do params
return (
<div>
<h1>Profil użytkownika {params.userId}</h1>
<p>Imię: {user.name}</p>
<p>Email: {user.email}</p>
</div>
);
}
// Przykład z wieloma parametrami:
// app/routes/users.$userId.posts.$postId.tsx
export async function loader({ params }: LoaderFunctionArgs) {
const { userId, postId } = params;
const post = await getPost(userId, postId);
return json({ post });
}
Diagram:
flowchart LR
A[/users/123] --> B{Router}
B --> C[users.$userId.tsx]
C --> D[params.userId = '123']
D --> E[loader otrzymuje params]
E --> F[Komponent używa useLoaderData]
G[/users/123/posts/456] --> H{Router}
H --> I[users.$userId.posts.$postId.tsx]
I --> J[params.userId = '123'<br/>params.postId = '456']
style C fill:#e6f3ff
style I fill:#e6f3ff
style D fill:#ffe6e6
style J fill:#ffe6e6
Materiały
↑ Powrót na góręRemix - Sekcja 4: Action i Mutacje Danych
21. Jak obsługiwać walidację formularzy po stronie serwera?
Odpowiedź w 30 sekund:
Walidacja w Remix odbywa się w funkcji action po stronie serwera. Sprawdzasz dane z formData, zwracasz błędy przez json() z odpowiednim statusem HTTP, a następnie wyświetlasz je w komponencie używając useActionData(). To zapewnia bezpieczeństwo i działa nawet bez JavaScript.
Odpowiedź w 2 minuty:
Walidacja po stronie serwera w Remix jest obowiązkowa i stanowi pierwszą linię obrony przed nieprawidłowymi danymi. Nigdy nie ufaj danym z przeglądarki - walidacja po stronie klienta to tylko udogodnienie UX, nie zabezpieczenie. W Remix cała logika walidacji znajduje się w funkcji action, co centralizuje reguły biznesowe i zapewnia spójność.
Proces walidacji wygląda następująco: odbierasz formData z requestu, wyciągasz wartości pól, sprawdzasz każde pole według reguł biznesowych, i jeśli znajdziesz błędy, zwracasz je jako obiekt JSON z kodem statusu 400 lub 422. Błędy powinny być strukturalne - zazwyczaj obiekt gdzie klucze to nazwy pól, a wartości to komunikaty błędów. To ułatwia mapowanie błędów do konkretnych pól w UI.
Zaleca się używanie bibliotek walidacyjnych jak Zod, Yup, czy superstruct zamiast ręcznego pisania walidacji. Dają one type safety, kompozycję reguł, i automatyczne generowanie komunikatów błędów. Zod szczególnie dobrze integruje się z TypeScript w Remix - możesz zdefiniować schema raz i używać jej zarówno do walidacji jak i type inference.
Ważne jest zwracanie odpowiednich kodów statusu HTTP: 400 dla błędów walidacji danych wejściowych, 422 dla błędów semantycznych (np. duplikat email), 500 dla błędów serwera. Remix automatycznie obsługuje te kody i wywołuje Error Boundary dla 4xx i 5xx gdy odpowiednie. Możesz też zwrócić sukces z kodem 200 i danymi (np. ID nowego rekordu) lub przekierowanie 302/303.
Przykład kodu:
// app/routes/register.tsx
import { json, redirect, type ActionFunctionArgs } from "@remix-run/node";
import { Form, useActionData, useNavigation } from "@remix-run/react";
import { z } from "zod";
import { createUser, getUserByEmail } from "~/models/user.server";
import { createUserSession } from "~/session.server";
// Schema walidacji z Zod
const RegisterSchema = z.object({
email: z
.string()
.min(1, "Email jest wymagany")
.email("Nieprawidłowy format email"),
password: z
.string()
.min(8, "Hasło musi mieć minimum 8 znaków")
.regex(/[A-Z]/, "Hasło musi zawierać wielką literę")
.regex(/[0-9]/, "Hasło musi zawierać cyfrę"),
confirmPassword: z.string(),
username: z
.string()
.min(3, "Nazwa użytkownika musi mieć minimum 3 znaki")
.max(20, "Nazwa użytkownika może mieć maksymalnie 20 znaków")
.regex(/^[a-zA-Z0-9_]+$/, "Tylko litery, cyfry i podkreślnik"),
terms: z
.literal("on", {
errorMap: () => ({ message: "Musisz zaakceptować regulamin" })
})
}).refine((data) => data.password === data.confirmPassword, {
message: "Hasła muszą być identyczne",
path: ["confirmPassword"]
});
type ActionData = {
errors?: {
email?: string;
password?: string;
confirmPassword?: string;
username?: string;
terms?: string;
form?: string;
};
};
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
// Konwersja FormData do obiektu
const rawData = {
email: formData.get("email"),
password: formData.get("password"),
confirmPassword: formData.get("confirmPassword"),
username: formData.get("username"),
terms: formData.get("terms")
};
// Walidacja z Zod
const result = RegisterSchema.safeParse(rawData);
if (!result.success) {
// Przekształć błędy Zod do formatu dla UI
const errors: ActionData["errors"] = {};
result.error.issues.forEach((issue) => {
const path = issue.path[0] as string;
errors[path] = issue.message;
});
return json<ActionData>(
{ errors },
{ status: 400 }
);
}
const { email, password, username } = result.data;
// Sprawdź czy użytkownik już istnieje
const existingUser = await getUserByEmail(email);
if (existingUser) {
return json<ActionData>(
{
errors: {
email: "Użytkownik z tym adresem email już istnieje"
}
},
{ status: 422 }
);
}
// Utwórz użytkownika
try {
const user = await createUser({ email, password, username });
// Utwórz sesję i przekieruj
return createUserSession({
request,
userId: user.id,
redirectTo: "/dashboard"
});
} catch (error) {
return json<ActionData>(
{
errors: {
form: "Wystąpił błąd podczas tworzenia konta. Spróbuj ponownie."
}
},
{ status: 500 }
);
}
}
export default function Register() {
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
return (
<div>
<h1>Rejestracja</h1>
<Form method="post">
{/* Globalny błąd formularza */}
{actionData?.errors?.form && (
<div className="alert alert-error">
{actionData.errors.form}
</div>
)}
<div className="form-group">
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
required
aria-invalid={actionData?.errors?.email ? true : undefined}
aria-describedby={actionData?.errors?.email ? "email-error" : undefined}
/>
{actionData?.errors?.email && (
<p className="error" id="email-error">
{actionData.errors.email}
</p>
)}
</div>
<div className="form-group">
<label htmlFor="username">Nazwa użytkownika:</label>
<input
type="text"
id="username"
name="username"
required
aria-invalid={actionData?.errors?.username ? true : undefined}
aria-describedby={actionData?.errors?.username ? "username-error" : undefined}
/>
{actionData?.errors?.username && (
<p className="error" id="username-error">
{actionData.errors.username}
</p>
)}
</div>
<div className="form-group">
<label htmlFor="password">Hasło:</label>
<input
type="password"
id="password"
name="password"
required
aria-invalid={actionData?.errors?.password ? true : undefined}
aria-describedby={actionData?.errors?.password ? "password-error" : undefined}
/>
{actionData?.errors?.password && (
<p className="error" id="password-error">
{actionData.errors.password}
</p>
)}
</div>
<div className="form-group">
<label htmlFor="confirmPassword">Potwierdź hasło:</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
required
aria-invalid={actionData?.errors?.confirmPassword ? true : undefined}
aria-describedby={actionData?.errors?.confirmPassword ? "confirm-error" : undefined}
/>
{actionData?.errors?.confirmPassword && (
<p className="error" id="confirm-error">
{actionData.errors.confirmPassword}
</p>
)}
</div>
<div className="form-group">
<label>
<input
type="checkbox"
name="terms"
aria-invalid={actionData?.errors?.terms ? true : undefined}
aria-describedby={actionData?.errors?.terms ? "terms-error" : undefined}
/>
Akceptuję regulamin
</label>
{actionData?.errors?.terms && (
<p className="error" id="terms-error">
{actionData.errors.terms}
</p>
)}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Rejestrowanie..." : "Zarejestruj się"}
</button>
</Form>
</div>
);
}
Diagram:
flowchart TD
A[Użytkownik wysyła formularz] --> B[action otrzymuje FormData]
B --> C[Parsowanie danych]
C --> D[Walidacja Schema Zod]
D --> E{Błędy walidacji?}
E -->|TAK| F[Zwróć json z błędami + status 400]
F --> G[useActionData w komponencie]
G --> H[Wyświetl błędy przy polach]
H --> I[Użytkownik poprawia dane]
I --> A
E -->|NIE| J[Walidacja biznesowa]
J --> K{Email już istnieje?}
K -->|TAK| L[Zwróć błąd + status 422]
L --> G
K -->|NIE| M[Zapis do bazy danych]
M --> N{Sukces?}
N -->|NIE| O[Zwróć błąd serwera + status 500]
O --> G
N -->|TAK| P[Utwórz sesję]
P --> Q[redirect do /dashboard]
style F fill:#FF5722
style L fill:#FF9800
style O fill:#F44336
style Q fill:#4CAF50
Materiały:
↑ Powrót na góręRemix - Sekcja 6: Sesje i Autentykacja
31. Jak chronić trasy wymagające uwierzytelnienia?
Odpowiedź w 30 sekund:
Trasy chronimy w loaderze wywołując funkcję requireUserId lub requireUser, która sprawdza sesję i albo zwraca dane użytkownika, albo rzuca redirect do strony logowania. Można też używać middleware patterns lub utility functions, które weryfikują uprawnienia i role użytkownika przed udostępnieniem danych.
Odpowiedź w 2 minuty:
Ochrona tras w Remix odbywa się głównie w loaderach, gdzie przed zwróceniem jakichkolwiek danych weryfikujemy tożsamość użytkownika. Najprostszy sposób to wywołanie requireUserId(request), która sprawdza sesję i albo zwraca user ID, albo rzuca redirect (throw redirect("/login")). Rzucenie redirecta w loaderze przerywa wykonanie i natychmiast przekierowuje użytkownika.
Dla bardziej zaawansowanych przypadków można implementować role-based access control (RBAC). Tworzymy funkcje typu requireAdmin, requireRole, które nie tylko sprawdzają czy użytkownik jest zalogowany, ale też czy ma odpowiednie uprawnienia. Jeśli nie ma, można przekierować do strony błędu 403 lub do dashboard.
Warto też przekazywać redirectTo parameter, aby po zalogowaniu użytkownik wrócił do oryginalnie żądanej strony. W nested routes ochrona w parent route automatycznie chroni wszystkie child routes, co pozwala na DRY (Don't Repeat Yourself) pattern.
Actions również wymagają ochrony - użytkownik może wysłać POST request bez dostępu do UI, więc zawsze weryfikuj sesję w action przed wykonaniem jakiejkolwiek operacji mutującej dane. Kombinacja loader + action protection zapewnia pełne zabezpieczenie route.
Przykład kodu:
// app/utils/auth.server.ts
import { redirect } from "@remix-run/node";
import { db } from "~/db.server";
import { getUserId } from "./session.server";
// Podstawowa ochrona - wymaga zalogowania
export async function requireUserId(
request: Request,
redirectTo: string = new URL(request.url).pathname
) {
const userId = await getUserId(request);
if (!userId) {
const searchParams = new URLSearchParams([["redirectTo", redirectTo]]);
throw redirect(`/login?${searchParams}`);
}
return userId;
}
// Ochrona z pobraniem pełnych danych użytkownika
export async function requireUser(request: Request) {
const userId = await requireUserId(request);
const user = await db.user.findUnique({
where: { id: userId },
select: { id: true, email: true, role: true, name: true },
});
if (!user) {
throw logout(request);
}
return user;
}
// Ochrona wymagająca roli administratora
export async function requireAdmin(request: Request) {
const user = await requireUser(request);
if (user.role !== "ADMIN") {
throw new Response("Brak uprawnień", { status: 403 });
}
return user;
}
// Ochrona z weryfikacją konkretnej roli
export async function requireRole(request: Request, role: string) {
const user = await requireUser(request);
if (user.role !== role) {
throw new Response("Brak uprawnień", { status: 403 });
}
return user;
}
// Ochrona z weryfikacją właściciela zasobu
export async function requireResourceOwner(
request: Request,
resourceId: string,
resourceType: "post" | "comment"
) {
const user = await requireUser(request);
// Sprawdź czy użytkownik jest właścicielem zasobu
const resource = await db[resourceType].findUnique({
where: { id: resourceId },
select: { userId: true },
});
if (!resource || resource.userId !== user.id) {
throw new Response("Brak dostępu do zasobu", { status: 403 });
}
return user;
}
// app/routes/dashboard.tsx - Prosta chroniona trasa
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { requireUser } from "~/utils/auth.server";
export async function loader({ request }: LoaderFunctionArgs) {
// Wymaga zalogowania - redirect jeśli niezalogowany
const user = await requireUser(request);
return json({ user });
}
export default function Dashboard() {
const { user } = useLoaderData<typeof loader>();
return (
<div>
<h1>Dashboard</h1>
<p>Witaj, {user.name}!</p>
</div>
);
}
// app/routes/admin.tsx - Trasa tylko dla adminów
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { requireAdmin } from "~/utils/auth.server";
export async function loader({ request }: LoaderFunctionArgs) {
// Tylko dla administratorów - 403 dla innych użytkowników
const admin = await requireAdmin(request);
const users = await db.user.findMany({
select: { id: true, email: true, name: true, role: true },
});
return json({ admin, users });
}
export default function AdminPanel() {
const { admin, users } = useLoaderData<typeof loader>();
return (
<div>
<h1>Panel Administratora</h1>
<p>Zalogowany jako: {admin.email}</p>
<h2>Użytkownicy ({users.length})</h2>
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} ({user.email}) - {user.role}
</li>
))}
</ul>
</div>
);
}
// app/routes/posts.$postId.edit.tsx - Ochrona właściciela zasobu
import { json, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node";
import { Form, useLoaderData } from "@remix-run/react";
import { requireUser, requireResourceOwner } from "~/utils/auth.server";
import { db } from "~/db.server";
export async function loader({ request, params }: LoaderFunctionArgs) {
const { postId } = params;
invariant(postId, "postId wymagany");
// Sprawdź czy użytkownik jest właścicielem posta
await requireResourceOwner(request, postId, "post");
const post = await db.post.findUnique({
where: { id: postId },
});
if (!post) {
throw new Response("Post nie znaleziony", { status: 404 });
}
return json({ post });
}
export async function action({ request, params }: ActionFunctionArgs) {
const { postId } = params;
invariant(postId, "postId wymagany");
// Ponowna weryfikacja w action (zawsze!)
const user = await requireResourceOwner(request, postId, "post");
const formData = await request.formData();
const title = formData.get("title");
const content = formData.get("content");
if (typeof title !== "string" || typeof content !== "string") {
return json({ error: "Nieprawidłowe dane" }, { status: 400 });
}
await db.post.update({
where: { id: postId },
data: { title, content },
});
return redirect(`/posts/${postId}`);
}
export default function EditPost() {
const { post } = useLoaderData<typeof loader>();
return (
<div>
<h1>Edytuj Post</h1>
<Form method="post">
<div>
<label>
Tytuł:
<input name="title" defaultValue={post.title} required />
</label>
</div>
<div>
<label>
Treść:
<textarea name="content" defaultValue={post.content} required />
</label>
</div>
<button type="submit">Zapisz</button>
</Form>
</div>
);
}
// app/routes/account.tsx - Parent route chroniąca wszystkie child routes
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { Outlet, useLoaderData } from "@remix-run/react";
import { requireUser } from "~/utils/auth.server";
export async function loader({ request }: LoaderFunctionArgs) {
// Ochrona parent route chroni automatycznie wszystkie child routes
const user = await requireUser(request);
return json({ user });
}
export default function Account() {
const { user } = useLoaderData<typeof loader>();
return (
<div>
<nav>
<h2>Konto użytkownika: {user.name}</h2>
<ul>
<li><Link to="/account/profile">Profil</Link></li>
<li><Link to="/account/settings">Ustawienia</Link></li>
<li><Link to="/account/billing">Płatności</Link></li>
</ul>
</nav>
<main>
{/* Child routes są automatycznie chronione */}
<Outlet />
</main>
</div>
);
}
Diagram:
flowchart TD
A[Żądanie chronionej trasy] --> B[Loader wykonany]
B --> C{requireUser/requireUserId}
C --> D[Sprawdź sesję]
D --> E{Sesja istnieje?}
E -->|Nie| F[throw redirect '/login?redirectTo=...']
E -->|Tak| G[Pobierz userId z sesji]
G --> H{User istnieje w DB?}
H -->|Nie| I[throw logout]
H -->|Tak| J{Sprawdź uprawnienia?}
J -->|requireAdmin| K{role === ADMIN?}
J -->|requireRole| L{role === wymagana?}
J -->|requireResourceOwner| M{userId === resourceUserId?}
J -->|Brak dodatkowych| N[Zwróć user]
K -->|Nie| O[throw Response 403]
K -->|Tak| N
L -->|Nie| O
L -->|Tak| N
M -->|Nie| O
M -->|Tak| N
N --> P[Pobierz dane dla route]
P --> Q[Renderuj komponent]
F --> R[Przekierowanie do logowania]
I --> S[Wylogowanie i redirect]
O --> T[Strona błędu 403]
Materiały:
↑ Powrót na góręRemix - Sekcja 3: Loader i Pobieranie Danych
13. Czym jest funkcja loader i jak działa pobieranie danych w Remix?
Odpowiedź w 30 sekund:
Funkcja loader to funkcja serwerowa eksportowana z pliku trasy, która wykonuje się przed renderowaniem komponentu i dostarcza dane do UI. Działa wyłącznie po stronie serwera, ma dostęp do bazy danych i tajnych kluczy, a dane zwracane przez nią są automatycznie dostępne w komponencie przez hook useLoaderData().
Odpowiedź w 2 minuty:
Loader to fundamentalny mechanizm pobierania danych w Remix, który rewolucjonizuje sposób, w jaki myślimy o ładowaniu danych w aplikacjach React. W przeciwieństwie do tradycyjnego podejścia z useEffect, gdzie dane pobierane są po renderowaniu komponentu, loader wykonuje się przed renderowaniem - podobnie jak getServerSideProps w Next.js, ale z większą elastycznością.
Loader jest funkcją asynchroniczną, która musi zostać nazwana loader i wyeksportowana z pliku trasy. Wykonuje się zawsze po stronie serwera podczas nawigacji początkowej (SSR) oraz po stronie serwera dla kolejnych nawigacji (zwraca JSON przez fetch). Ma pełny dostęp do wszystkich zasobów serwerowych - bazy danych, API, systemu plików, zmiennych środowiskowych - bez obawy o wycieki danych do klienta.
Kluczową zaletą jest automatyczna rewalidacja: Remix wie, kiedy dane mogły się zmienić (po action, po nawigacji, przy używaniu useFetcher) i automatycznie wywołuje loadery ponownie. Wszystko to działa z natywną nawigacją przeglądarki - wsparcie dla przycisku "wstecz", otwieranie w nowej karcie, progressive enhancement.
Loader może zwracać różne typy odpowiedzi: JSON (najczęstsze), Response z custom headers, streamy (z defer), przekierowania, błędy. Remix automatycznie serializuje i deserializuje dane, obsługuje błędy przez najbliższy ErrorBoundary i zapewnia TypeScript type safety między loaderem a komponentem.
Przykład kodu:
// app/routes/products.$productId.tsx
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { db } from "~/db.server";
// Loader - wykonuje się po stronie serwera
export async function loader({ params, request }: LoaderFunctionArgs) {
const { productId } = params;
// Możemy bezpiecznie używać tajnych kluczy
const product = await db.product.findUnique({
where: { id: productId },
include: { reviews: true, category: true }
});
if (!product) {
// Remix automatycznie obsłuży to przez ErrorBoundary
throw new Response("Produkt nie znaleziony", { status: 404 });
}
// Możemy ustawić custom headers
return json(product, {
headers: {
"Cache-Control": "public, max-age=300"
}
});
}
// Komponent - dane są już dostępne przy pierwszym renderowaniu
export default function ProductPage() {
const product = useLoaderData<typeof loader>();
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>Cena: {product.price} PLN</p>
<div>
<h2>Kategoria: {product.category.name}</h2>
<h3>Opinie ({product.reviews.length})</h3>
{product.reviews.map(review => (
<div key={review.id}>{review.text}</div>
))}
</div>
</div>
);
}
Diagram:
sequenceDiagram
participant Browser
participant RemixServer
participant Loader
participant Database
participant Component
Browser->>RemixServer: GET /products/123
RemixServer->>Loader: Wywołaj loader({ params, request })
Loader->>Database: Pobierz dane produktu
Database-->>Loader: Zwróć dane
Loader-->>RemixServer: return json(data)
RemixServer->>Component: Renderuj z danymi
Component-->>Browser: HTML z danymi (SSR)
Note over Browser,Component: Kolejna nawigacja (client-side)
Browser->>RemixServer: Fetch /products/456?_data=routes/products.$productId
RemixServer->>Loader: Wywołaj loader
Loader->>Database: Pobierz dane
Database-->>Loader: Zwróć dane
Loader-->>RemixServer: return json(data)
RemixServer-->>Browser: JSON
Browser->>Component: Re-render z nowymi danymi
Materiały:
↑ Powrót na góręRemix - Sekcja 5: Obsługa Błędów
25. Jak działa ErrorBoundary w Remix?
Odpowiedź w 30 sekund: ErrorBoundary w Remix to komponent eksportowany z pliku route, który przechwytuje wszystkie błędy renderowania i błędy rzucone w loaderach/actions tej trasy i jej potomków. Automatycznie zastępuje standardowy widok strony interfejsem błędu, gdy coś pójdzie nie tak. Od Remix v2, ErrorBoundary obsługuje zarówno błędy nieprzechwycone, jak i odpowiedzi HTTP z błędami (łącząc funkcjonalność CatchBoundary).
Odpowiedź w 2 minuty: ErrorBoundary to specjalna funkcja/komponent eksportowana z pliku route w Remix, który działa jako "siatka bezpieczeństwa" dla całej trasy i jej potomków. Kiedy loader, action lub komponent rzuci błąd, Remix automatycznie renderuje ErrorBoundary zamiast normalnego UI. To podejście jest bardziej granularne niż tradycyjne React Error Boundaries - każda trasa może mieć własny ErrorBoundary.
W Remix v2 nastąpiła istotna zmiana - ErrorBoundary został połączony z CatchBoundary. Teraz jeden komponent obsługuje wszystkie błędy, niezależnie czy to błędy JavaScript (Error), czy odpowiedzi Response z kodem błędu. Hook useRouteError() zwraca obiekt błędu, który można sprawdzić za pomocą isRouteErrorResponse() aby rozróżnić typy błędów.
ErrorBoundary działa kaskadowo - jeśli trasa nie ma własnego ErrorBoundary, błąd "wypływa" do najbliższego rodzica, który go ma. Dzięki temu można mieć globalny ErrorBoundary w root.tsx oraz bardziej szczegółowe w poszczególnych trasach. To pozwala na kontekstową obsługę błędów - np. błąd w trasie produktu może pokazać "Produkt nie znaleziony", podczas gdy globalny ErrorBoundary obsłuży nieoczekiwane błędy systemowe.
Ważną zaletą tego podejścia jest to, że nawet gdy część aplikacji zawiedzie, reszta layoutu (np. nawigacja, stopka) nadal może być renderowana, o ile błąd jest przechwycony na odpowiednim poziomie zagnieżdżenia tras.
Przykład kodu:
// app/routes/users.$userId.tsx
import { json, LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData, useRouteError, isRouteErrorResponse } from "@remix-run/react";
export async function loader({ params }: LoaderFunctionArgs) {
const user = await db.user.findUnique({
where: { id: params.userId }
});
if (!user) {
// Rzuć response z kodem 404
throw json(
{ message: "Użytkownik nie został znaleziony" },
{ status: 404 }
);
}
// Symulacja błędu serwera
if (user.status === "corrupted") {
throw new Error("Dane użytkownika są uszkodzone");
}
return json({ user });
}
// Normalny komponent - renderowany gdy wszystko OK
export default function UserPage() {
const { user } = useLoaderData<typeof loader>();
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}
// ErrorBoundary - renderowany gdy wystąpi błąd
export function ErrorBoundary() {
const error = useRouteError();
// Sprawdź czy to błąd Response (404, 500, etc.)
if (isRouteErrorResponse(error)) {
return (
<div className="error-container">
<h1>{error.status} {error.statusText}</h1>
<p>{error.data?.message || "Wystąpił błąd"}</p>
{error.status === 404 && (
<a href="/users">Powrót do listy użytkowników</a>
)}
</div>
);
}
// Błąd JavaScript (Error object)
if (error instanceof Error) {
return (
<div className="error-container">
<h1>Nieoczekiwany błąd aplikacji</h1>
<p>{error.message}</p>
<pre>{error.stack}</pre>
</div>
);
}
// Nieznany typ błędu
return (
<div className="error-container">
<h1>Nieznany błąd</h1>
<p>Przepraszamy, coś poszło nie tak.</p>
</div>
);
}
Diagram:
flowchart TD
A[Request do trasy] --> B{Loader/Action wykonany}
B -->|Sukces| C[Renderuj normalny komponent]
B -->|Błąd rzucony| D{Trasa ma ErrorBoundary?}
D -->|Tak| E[Renderuj lokalny ErrorBoundary]
D -->|Nie| F{Rodzic ma ErrorBoundary?}
F -->|Tak| G[Renderuj ErrorBoundary rodzica]
F -->|Nie| H[Idź wyżej w hierarchii]
H --> F
E --> I[Użytkownik widzi UI błędu]
G --> I
style E fill:#f9f,stroke:#333
style G fill:#f9f,stroke:#333
style C fill:#9f9,stroke:#333
Materiały:
- Remix Error Handling Documentation
- Remix v2 Error Boundary Changes
- Error Handling in Remix - Kent C. Dodds
Remix - Sekcja 7: Stylowanie i Assets
33. Jakie są strategie stylowania w Remix (CSS Modules, Tailwind, CSS-in-JS)?
Odpowiedź w 30 sekund: Remix wspiera wszystkie popularne strategie stylowania, ale faworyzuje tradycyjne pliki CSS ze względu na optymalizację i progressive enhancement. Możesz używać zwykłych plików CSS, CSS Modules, Tailwind CSS, Sass, Less, czy CSS-in-JS (Styled Components, Emotion), ale każde rozwiązanie ma różne implikacje dla wydajności i doświadczenia użytkownika.
Odpowiedź w 2 minuty:
Remix oferuje elastyczne podejście do stylowania, ale ze względu na swoją filozofię progressive enhancement szczególnie promuje tradycyjne pliki CSS. Każda trasa może eksportować funkcję links, która określa, które arkusze stylów powinny być załadowane dla danej trasy. To pozwala na automatyczne code-splitting stylów i ich preloadowanie.
CSS Modules są w pełni wspierane i działają podobnie jak w innych frameworkach - wystarczy użyć rozszerzenia .module.css i importować style jako obiekt JavaScript. Remix automatycznie generuje unikalne nazwy klas i obsługuje eksport przez funkcję links.
Tailwind CSS jest bardzo popularnym wyborem w ekosystemie Remix. Po zainstalowaniu i skonfigurowaniu PostCSS, Tailwind integruje się bezproblemowo z systemem budowania Remix. Możesz używać klas utility bezpośrednio w JSX, a Remix zadba o optymalizację i minifikację CSS.
CSS-in-JS (jak Styled Components czy Emotion) również działa, ale wymaga dodatkowej konfiguracji, szczególnie dla server-side rendering. Musisz zadbać o ekstrakcję stylów na serwerze i hydratację na kliencie. Choć możliwe, to podejście jest mniej zalecane w Remix ze względu na dodatkową złożoność i potencjalny wpływ na wydajność, ponieważ style są generowane w JavaScript podczas runtime zamiast być statycznymi plikami CSS.
Przykład kodu:
// 1. Zwykły CSS - app/styles/dashboard.css
import type { LinksFunction } from "@remix-run/node";
import dashboardStyles from "~/styles/dashboard.css";
export const links: LinksFunction = () => [
{ rel: "stylesheet", href: dashboardStyles },
];
// 2. CSS Modules - app/styles/card.module.css
import styles from "~/styles/card.module.css";
export default function Card() {
return <div className={styles.card}>Zawartość karty</div>;
}
// 3. Tailwind CSS - bezpośrednio w JSX
export default function Button() {
return (
<button className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Kliknij mnie
</button>
);
}
// 4. CSS-in-JS (Styled Components) - wymaga dodatkowej konfiguracji
import styled from "styled-components";
const StyledButton = styled.button`
padding: 0.5rem 1rem;
background-color: #3b82f6;
color: white;
border-radius: 0.25rem;
&:hover {
background-color: #2563eb;
}
`;
export default function Button() {
return <StyledButton>Kliknij mnie</StyledButton>;
}
// 5. Sass/SCSS - app/styles/theme.scss
import type { LinksFunction } from "@remix-run/node";
import themeStyles from "~/styles/theme.scss";
export const links: LinksFunction = () => [
{ rel: "stylesheet", href: themeStyles },
];
Diagram:
flowchart TD
A[Strategie stylowania w Remix] --> B[Tradycyjny CSS]
A --> C[CSS Modules]
A --> D[Tailwind CSS]
A --> E[CSS-in-JS]
A --> F[Preprocesory CSS]
B --> B1[Zwykłe pliki .css]
B --> B2[Funkcja links]
B --> B3[Route-based splitting]
C --> C1[.module.css]
C --> C2[Scoped class names]
C --> C3[Import jako obiekt]
D --> D1[Utility classes]
D --> D2[PostCSS config]
D --> D3[JIT compilation]
E --> E1[Styled Components]
E --> E2[Emotion]
E --> E3[SSR config wymagana]
F --> F1[Sass/SCSS]
F --> F2[Less]
F --> F3[PostCSS]
style B fill:#90EE90
style C fill:#90EE90
style D fill:#90EE90
style E fill:#FFB6C1
style F fill:#90EE90
Materiały
↑ Powrót na góręRemix - Sekcja 8: Optymalizacja i Wydajność
36. Jak Remix optymalizuje ładowanie danych dzięki nested routing?
Odpowiedź w 30 sekund: Remix wykorzystuje nested routing do równoległego ładowania danych dla wszystkich zagnieżdżonych tras jednocześnie. Zamiast kaskadowego ładowania (rodzic → dziecko → wnuk), Remix identyfikuje wszystkie dopasowane trasy i wywołuje ich loadery równolegle, co znacząco skraca całkowity czas ładowania.
Odpowiedź w 2 minuty: W tradycyjnych frameworkach routing zagnieżdżony często prowadzi do tzw. "waterfall effect", gdzie każdy poziom zagnieżdżenia musi załadować swoje dane przed przejściem do kolejnego poziomu. Remix radykalnie zmienia to podejście poprzez wykorzystanie informacji o strukturze tras podczas kompilacji.
Gdy użytkownik nawiguje do zagnieżdżonej trasy, Remix natychmiast identyfikuje wszystkie komponenty, które będą renderowane (od roota przez wszystkie zagnieżdżone layouty aż do docelowej strony) i wywołuje wszystkie ich loadery równocześnie w jednym żądaniu. Na przykład, dla trasy /dashboard/settings/profile, Remix wywoła równolegle loadery z root.tsx, dashboard.tsx, dashboard.settings.tsx oraz dashboard.settings.profile.tsx.
Dodatkowo, Remix optymalizuje ponowne ładowanie danych podczas nawigacji między trasami o wspólnych rodzicach. Jeśli przechodzisz z /dashboard/settings/profile do /dashboard/settings/notifications, Remix ponownie załaduje tylko dane dla komponentu notifications.tsx, gdyż dane z root.tsx, dashboard.tsx i settings.tsx pozostają aktualne. Ta inteligentna rewalidacja znacząco zmniejsza ilość przesyłanych danych i poprawia wydajność aplikacji.
Mechanizm ten działa automatycznie - nie wymaga dodatkowej konfiguracji ze strony dewelopera. Remix wykorzystuje swoją wiedzę o strukturze routingu i automatycznie optymalizuje przepływ danych.
Przykład kodu:
// app/routes/dashboard.tsx - Loader rodzica
export async function loader({ request }: LoaderFunctionArgs) {
console.log('Dashboard loader - wywołany równolegle');
const user = await getUser(request);
return json({ user });
}
// app/routes/dashboard.settings.tsx - Loader zagnieżdżony poziom 1
export async function loader({ request }: LoaderFunctionArgs) {
console.log('Settings loader - wywołany równolegle z dashboard');
const settings = await getUserSettings(request);
return json({ settings });
}
// app/routes/dashboard.settings.profile.tsx - Loader zagnieżdżony poziom 2
export async function loader({ request }: LoaderFunctionArgs) {
console.log('Profile loader - wywołany równolegle z pozostałymi');
const profile = await getUserProfile(request);
return json({ profile });
}
// Wszystkie trzy loadery powyżej są wywoływane RÓWNOCZEŚNIE,
// a nie kaskadowo jak w tradycyjnych rozwiązaniach
export default function Profile() {
const { profile } = useLoaderData<typeof loader>();
// Dashboard i Settings dane są dostępne przez useRouteLoaderData()
return <div>{profile.name}</div>;
}
Diagram:
flowchart LR
A[Nawigacja do /dashboard/settings/profile] --> B{Remix identyfikuje wszystkie trasy}
B --> C[root.tsx loader]
B --> D[dashboard.tsx loader]
B --> E[dashboard.settings.tsx loader]
B --> F[dashboard.settings.profile.tsx loader]
C --> G[Wszystkie loadery wykonują się równolegle]
D --> G
E --> G
F --> G
G --> H[Renderowanie po zakończeniu wszystkich loaderów]
Materiały
↑ Powrót na góręRemix - Section 9: Deployment i Konfiguracja
40. Jakie adaptery deployment oferuje Remix (Vercel, Cloudflare, Node)?
Odpowiedź w 30 sekund:
Remix oferuje różne adaptery deployment dostosowane do środowisk hostingowych: @remix-run/node dla tradycyjnych serwerów Node.js, @remix-run/vercel dla platformy Vercel, @remix-run/cloudflare-pages i @remix-run/cloudflare-workers dla Cloudflare, oraz adaptery dla Netlify, Deno, Architect i innych platform. Każdy adapter tłumaczy Remix na specyficzne API danego środowiska.
Odpowiedź w 2 minuty: Remix wykorzystuje architekturę adapterów, która pozwala na deployment aplikacji do różnych środowisk bez zmian w kodzie aplikacji. Główne adaptery to:
Adaptery Serverless i Edge:
@remix-run/vercel- dla Vercel (zarówno serverless functions jak i Edge Functions)@remix-run/cloudflare-pages- dla Cloudflare Pages@remix-run/cloudflare-workers- dla Cloudflare Workers@remix-run/netlify- dla Netlify Functions@remix-run/architect- dla AWS (API Gateway + Lambda)
Adaptery dla tradycyjnych serwerów:
@remix-run/node- dla Node.js (Express, Fastify, etc.)@remix-run/deno- dla Deno Deploy
Każdy adapter implementuje ten sam interface Remix, ale dostosowuje go do specyficznych wymagań platformy. Na przykład, adapter Cloudflare Workers wykorzystuje Workers KV do cache'owania, podczas gdy adapter Node.js może używać filesystem lub Redis. Adapter odpowiada za inicjalizację serwera, obsługę requestów HTTP, zarządzanie sesjami i cookies oraz dostęp do zasobów specyficznych dla platformy (zmienne środowiskowe, storage, etc.).
Wybór adaptera wpływa na dostępne funkcjonalności - edge adaptery (Cloudflare, Vercel Edge) oferują niską latencję globalnie, ale mają ograniczenia runtime, podczas gdy Node.js daje pełną kontrolę i możliwość użycia dowolnych bibliotek npm.
Przykład kodu:
// remix.config.js - konfiguracja dla różnych adapterów
// 1. Node.js (tradycyjny serwer)
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
serverModuleFormat: "cjs",
server: "./server.js", // Własny serwer Express/Fastify
};
// server.js - przykład z Express
import { createRequestHandler } from "@remix-run/express";
import express from "express";
const app = express();
app.all("*", createRequestHandler({ build: require("./build") }));
// 2. Vercel - automatyczna konfiguracja
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
serverModuleFormat: "esm",
// Vercel automatycznie wykrywa Remix
};
// 3. Cloudflare Workers
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
serverModuleFormat: "esm",
server: "./server.ts",
serverConditions: ["workerd", "worker", "browser"],
serverDependenciesToBundle: "all", // Bundluj wszystko dla Workers
serverMainFields: ["browser", "module", "main"],
serverMinify: true,
serverPlatform: "neutral",
};
// server.ts dla Cloudflare Workers
import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";
import * as build from "@remix-run/dev/server-build";
export const onRequest = createPagesFunctionHandler({
build,
getLoadContext: (context) => ({
// Dostęp do Cloudflare bindings
env: context.env,
cloudflare: {
cf: context.request.cf,
ctx: context,
},
}),
});
// 4. Netlify
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
serverModuleFormat: "cjs",
// Netlify automatycznie wykrywa Remix
};
// Użycie context specyficznego dla platformy w loaderze
// app/routes/_index.tsx
import type { LoaderFunctionArgs } from "@remix-run/cloudflare";
export async function loader({ context }: LoaderFunctionArgs) {
// Cloudflare Workers KV
const value = await context.env.MY_KV.get("key");
// Vercel KV (Redis)
// const value = await context.kv.get("key");
// Node.js - filesystem lub dowolna baza
// const value = await db.query("SELECT ...");
return { value };
}
// Porównanie capabilities różnych adapterów
interface AdapterCapabilities {
edge: boolean; // Czy działa na edge
coldStart: string; // Czas cold start
maxDuration: string; // Max czas wykonania
runtime: string; // Dostępne runtime APIs
}
const capabilities: Record<string, AdapterCapabilities> = {
"cloudflare-workers": {
edge: true,
coldStart: "~0ms",
maxDuration: "CPU time limited",
runtime: "V8 isolates - Web APIs only"
},
"vercel-edge": {
edge: true,
coldStart: "~0ms",
maxDuration: "30s",
runtime: "V8 isolates - Web APIs only"
},
"node": {
edge: false,
coldStart: "Depends on host",
maxDuration: "Unlimited",
runtime: "Full Node.js APIs"
},
"vercel-serverless": {
edge: false,
coldStart: "~100-300ms",
maxDuration: "10s (hobby), 60s (pro)",
runtime: "Full Node.js APIs"
}
};
Diagram:
flowchart TD
A[Remix App Code] --> B{Adapter Layer}
B -->|@remix-run/node| C[Node.js Server]
B -->|@remix-run/vercel| D[Vercel Platform]
B -->|@remix-run/cloudflare-*| E[Cloudflare Platform]
B -->|@remix-run/netlify| F[Netlify Platform]
C --> C1[Express/Fastify]
C --> C2[Full Node.js APIs]
C --> C3[Własny hosting]
D --> D1[Serverless Functions]
D --> D2[Edge Functions]
D --> D3[Vercel KV/Blob]
E --> E1[Workers/Pages]
E --> E2[KV/D1/R2]
E --> E3[Global Edge Network]
F --> F1[Netlify Functions]
F --> F2[Edge Functions]
F --> F3[Netlify Blobs]
style B fill:#ff0
style C fill:#9f9
style D fill:#99f
style E fill:#f99
style F fill:#9ff
Diagram architektury adaptera:
flowchart LR
subgraph App["Aplikacja Remix"]
R[Routes]
L[Loaders]
A[Actions]
end
subgraph Adapter["Adapter Layer"]
H[HTTP Handler]
S[Session Storage]
C[Cookie Parser]
CT[Context Builder]
end
subgraph Platform["Specyfika Platformy"]
N1[Node: fs, process]
N2[Vercel: env, KV]
N3[Cloudflare: bindings, KV]
N4[Netlify: functions, env]
end
Request[HTTP Request] --> H
H --> CT
CT --> N1 & N2 & N3 & N4
CT --> L & A
L & A --> Response[HTTP Response]
style Adapter fill:#ff9
style Platform fill:#9ff
Materiały
↑ Powrót na górę