Миграция с Remix

Архитектурные различия

Remix и Fresh имеют принципиальные различия в подходе к рендерингу и маршрутизации. Remix основан на React и использует концепцию nested routes с серверными и клиентскими загрузками данных через loader и action. Fresh, напротив, построен на Deno и использует islands architecture, где рендеринг по умолчанию выполняется на сервере, а интерактивные части страницы выделяются как независимые “острова”.

Ключевое различие заключается в том, что в Fresh не требуется отдельный клиентский бандл для всего приложения. Каждая интерактивная компонента компилируется отдельно, что снижает объем загружаемого кода и ускоряет загрузку страниц.

Структура проекта

В Remix проект структурирован вокруг папки routes, где каждая страница или вложенный маршрут определяется отдельным файлом. Fresh использует папку routes аналогично, но каждая страница может содержать компонент Island, который добавляет интерактивность без полной загрузки React на клиент.

Пример миграции маршрута:

Remix:

// app/routes/index.jsx
import { json, LoaderFunction } from "@remix-run/node";

export const loader: LoaderFunction = async () => {
  return json({ message: "Привет, Remix!" });
};

export default function Index() {
  const data = useLoaderData();
  return <h1>{data.message}</h1>;
}

Fresh:

// routes/index.tsx
import { Handlers, PageProps } from "$fresh/server.ts";

export const handler: Handlers = {
  GET(req, ctx) {
    return ctx.render({ message: "Привет, Fresh!" });
  },
};

export default function Home({ data }: PageProps) {
  return <h1>{data.message}</h1>;
}

Разница в том, что Fresh использует серверный рендеринг напрямую через Handlers, а данные передаются через объект data.

Работа с формами и действиями

В Remix формы могут использовать action для обработки POST-запросов и мутаций данных. В Fresh формы обрабатываются через HTTP-обработчики (Handlers) или стандартные API-эндпойнты. Пример POST-запроса:

// routes/contact.tsx
import { Handlers, PageProps } from "$fresh/server.ts";

export const handler: Handlers = {
  POST(req, ctx) {
    const formData = await req.formData();
    const name = formData.get("name");
    return new Response(`Принято имя: ${name}`);
  },
};

export default function Contact() {
  return (
    <form method="POST">
      <input name="name" type="text" />
      <button type="submit">Отправить</button>
    </form>
  );
}

Все обработчики Fresh работают на сервере и возвращают Response, что упрощает миграцию, так как логика не разделяется на клиентскую и серверную части.

Интерактивные компоненты (Islands)

В Remix интерактивность реализуется через React-компоненты с состоянием и эффектами. Fresh предлагает острова, которые включаются только там, где нужна интерактивность.

Пример интерактивного острова:

// islands/Counter.tsx
import { useState } from "preact/hooks";

export default function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onCl ick={() => setCount(count + 1)}>
      Нажато {count} раз
    </button>
  );
}

Использение на странице:

// routes/index.tsx
import Counter from "../islands/Counter.tsx";

export default function Home() {
  return (
    <>
      <h1>Пример острова</h1>
      <Counter />
    </>
  );
}

Такая архитектура уменьшает объем JavaScript на клиенте, так как остальные элементы страницы остаются статическими.

Работа с данными и состоянием

Remix активно использует loader и useLoaderData для загрузки данных, а Fresh применяет контроллеры в Handlers и передает данные в компонент через PageProps. Для глобального состояния и кэширования можно использовать useState внутри островов или сторонние state-менеджеры, но важно помнить, что большая часть рендеринга остаётся серверной.

Роутинг и динамические маршруты

Dynamically generated routes в Remix — это файлы с квадратными скобками [id].jsx. В Fresh аналог реализуется через динамические сегменты в имени файла [id].tsx.

Пример:

// routes/posts/[id].tsx
import { Handlers, PageProps } from "$fresh/server.ts";

export const handler: Handlers = {
  GET(req, ctx) {
    const { id } = ctx.params;
    return ctx.render({ id });
  },
};

export default function Post({ data }: PageProps) {
  return <h1>Пост {data.id}</h1>;
}

Контекст ctx.params напрямую предоставляет параметры маршрута без необходимости использовать отдельные хуки.

Асинхронные данные и кэширование

Fresh позволяет использовать Deno KV, сторонние API или fetch прямо в Handlers. Для кэширования можно применять заголовки HTTP (Cache-Control) или middleware-подобные функции.

Пример кэширования:

export const handler: Handlers = {
  GET(req, ctx) {
    const headers = new Headers();
    headers.set("Cache-Control", "max-age=60");
    return new Response("Данные закэшированы на 60 секунд", { headers });
  },
};

Это упрощает управление данными по сравнению с Remix, где приходится комбинировать клиентский и серверный кэш.

Подключение сторонних библиотек

Большинство React-библиотек можно использовать внутри островов. При этом важно учитывать, что острова используют Preact, поэтому некоторые специфичные React-хуки или контексты могут требовать адаптации.

Итоговые рекомендации по миграции

  • Каждый маршрут Remix конвертируется в routes Fresh с обработчиком Handlers.
  • Интерактивные компоненты выносятся в islands для минимизации клиентского кода.
  • Динамические параметры маршрутов получают через ctx.params.
  • POST-запросы и формы обрабатываются через Handlers, возвращая Response.
  • Использование сторонних библиотек возможно внутри островов, но проверяется совместимость с Preact.

Такая структура обеспечивает чистый серверный рендеринг с минимальной загрузкой JavaScript на клиенте и полностью меняет подход к построению приложений по сравнению с Remix.