Миграция с Next.js

Миграция с Next.js на Fresh требует понимания архитектурных различий между обоими фреймворками. Next.js ориентирован на универсальный рендеринг (SSR и SSG) с сильной интеграцией React, тогда как Fresh использует нативные веб-компоненты и Islands Architecture, минимизируя JavaScript на клиенте и ориентируясь на Deno. Это накладывает определённые ограничения и открывает возможности для оптимизации производительности.


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

В Next.js проект имеет стандартную структуру:

/pages
/components
/public
/styles

В Fresh структура проекта отличается:

/routes
/components
/islands
/static
  • routes/ — основной каталог для страниц, каждая страница соответствует файлу с именем маршрута (/routes/index.tsx/).
  • components/ — статические компоненты, которые рендерятся только на сервере.
  • islands/ — интерактивные компоненты, рендерятся на клиенте только при необходимости.
  • static/ — статические ресурсы, аналог public в Next.js.

Понимание этой структуры позволяет правильно распределять код между сервером и клиентом.


Маршрутизация

Next.js использует файловую маршрутизацию в pages и динамические маршруты через [param].tsx. Fresh применяет похожий подход:

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

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

export default function PostPage({ data }: PageProps<Post>) {
  return <div>{data.title}</div>;
}

Особенности:

  • Контекст ctx содержит параметры маршрута и метод render() для передачи данных в компонент.
  • Все маршруты Fresh по умолчанию рендерятся на сервере.
  • Для интерактивных частей нужно использовать islands, которые подключаются как отдельные компоненты.

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

Next.js использует getServerSideProps, getStaticProps и getStaticPaths для SSR/SSG. В Fresh эквивалент реализуется через Handlers:

export const handler: Handlers = {
  async GET(req, ctx) {
    const data = await fetchData();
    return ctx.render(data);
  },
};
  • ctx.render(data) передаёт данные в компонент страницы.
  • Для динамического рендеринга используется ctx.params.
  • Для SSG в Fresh можно использовать функцию export const prerender = true; в маршруте, что позволяет заранее рендерить страницы на этапе сборки.

Перенос компонентов

Next.js: большинство компонентов React, интерактивные компоненты рендерятся как обычные React-компоненты.

Fresh: компоненты делятся на серверные и клиентские (Islands):

// components/PostList.tsx - серверный компонент
export default function PostList({ posts }: { posts: Post[] }) {
  return (
    <ul>
      {posts.map((post) => (
        <li>{post.title}</li>
      ))}
    </ul>
  );
}

// islands/LikeButton.tsx - клиентский компонент
import { useState } from "preact/hooks";

export default function LikeButton() {
  const [count, setCount] = useState(0);
  return <button onCl ick={() => setCount(count + 1)}>Like {count}</button>;
}

Особенности миграции:

  • Компоненты, которые были интерактивными в Next.js, переводятся в Islands.
  • Серверные компоненты рендерятся на сервере и могут включать данные, полученные через Handlers.
  • Fresh использует Preact, что требует замены импорта React на Preact и адаптации хуков.

Стилизация

Next.js поддерживает CSS Modules, styled-jsx, Tailwind и другие подходы. Fresh работает с любым CSS, но нет встроенной поддержки CSS-in-JS:

// routes/index.tsx
import "../static/styles.css";

export default function Home() {
  return <h1 class="title">Привет, Fresh!</h1>;
}

Для Tailwind настройка аналогична Deno: подключение через PostCSS и build-скрипт.


Обработка API

Next.js: API routes через pages/api. Fresh: API создаются как серверные маршруты без отдельного каталога:

// routes/api/posts.ts
import { Handlers } from "$fresh/server.ts";

export const handler: Handlers = {
  async GET() {
    const posts = await fetchPosts();
    return new Response(JSON.stringify(posts), {
      headers: { "Content-Type": "application/json" },
    });
  },
};

Особенности:

  • Любой маршрут может быть API.
  • Нет отдельного деления на API и страницы.
  • Можно комбинировать SSR и API в одном маршруте при необходимости.

Миграция состояния и хранилищ

Next.js часто использует Redux, Context API или SWR. В Fresh:

  • Клиентские состояния должны быть в Islands (Preact hooks).
  • Для серверного состояния можно передавать данные через Handlers.
  • Для кеширования данных на клиенте используют useEffect и fetch внутри Islands.

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

  • React-библиотеки, использующие Virtual DOM, нужно переписывать под Preact.
  • Чисто JS-библиотеки работают без изменений.
  • Модули Node.js заменяются на Deno-совместимые или URL-импорты (https://deno.land/...).

Практические советы

  • Разделять компоненты на серверные и интерактивные с самого начала.
  • Проверять, что динамические маршруты правильно используют ctx.params.
  • Переходить на Deno-модули и URL-импорты для всех зависимостей.
  • Встраивать Tailwind через PostCSS, если проект использует утилиты CSS.
  • Использовать export const prerender = true для страниц, которые можно заранее рендерить.

Fresh позволяет существенно оптимизировать загрузку и производительность за счёт уменьшения клиентского JavaScript. Правильное разделение компонентов и маршрутов является ключевым этапом при миграции с Next.js.