Fresh построен вокруг идеи серверного рендеринга без сборки и минимального JavaScript на клиенте. В основе клиентской части используется Preact — лёгкая и быстрая альтернатива React с совместимым API. Это означает, что практически все библиотеки, экосистема и паттерны React применимы и во Fresh, с учётом особенностей его архитектуры.
Ключевое отличие Fresh заключается в том, что JavaScript на клиенте загружается только для интерактивных компонентов, называемых islands. Все остальные части страницы остаются статическими HTML-фрагментами, отрендеренными на сервере.
Islands — это Preact-компоненты, которые:
Любая Preact-библиотека используется внутри islands, а не на уровне всей страницы.
Пример структуры:
routes/
index.tsx
islands/
Counter.tsx
// 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>
);
}
Этот компонент может использовать любые хуки и сторонние библиотеки, совместимые с Preact.
Fresh поставляется с preact и preact/hooks
из коробки. Поддерживаются все базовые хуки:
useStateuseEffectuseMemouseCallbackuseRefuseContextimport { useEffect, useState } from "preact/hooks";
export function Timer() {
const [time, setTime] = useState(0);
useEffect(() => {
const id = setInterval(() => setTime(t => t + 1), 1000);
return () => clearInterval(id);
}, []);
return <span>{time}</span>;
}
Хуки выполняются только на клиенте, что важно учитывать при работе с API браузера.
Zustand отлично подходит для Fresh благодаря отсутствию привязки к React DOM.
import { create } from "zustand";
export const useStore = create(set => ({
count: 0,
inc: () => set(state => ({ count: state.count + 1 })),
}));
Использование в island:
import { useStore } from "../store.ts";
export function Counter() {
const { count, inc } = useStore();
return <button onCl ick={inc}>{count}</button>;
}
Каждый island получает доступ к общему состоянию, если они используются на одной странице.
Контексты работают стандартным образом, но должны быть инициализированы внутри islands:
import { createContext } from "preact";
import { useContext } from "preact/hooks";
const ThemeContext = createContext("light");
export function ThemeProvider({ children }) {
return (
<ThemeContext.Provider value="dark">
{children}
</ThemeContext.Provider>
);
}
Большинство библиотек, не использующих специфичный React DOM API, работают без изменений:
Библиотеки, жёстко привязанные к react-dom, требуют
адаптации или не работают.
Signals — рекомендуемый способ реактивного состояния во Fresh.
import { signal } from "@preact/signals";
const count = signal(0);
export function Counter() {
return (
<button onCl ick={() => count.value++}>
{count}
</button>
);
}
Преимущества:
Для интерактивных форм используются islands:
import { useState } from "preact/hooks";
export function LoginForm() {
const [error, setError] = useState("");
return (
<form>
<input type="email" />
<button>Войти</button>
{error && <p>{error}</p>}
</form>
);
}
Fresh поощряет перенос логики в обработчики POST:
export const handler = {
async POST(req) {
const data = await req.formData();
return new Response("OK");
},
};
UI-библиотеки применяются только к клиентской части, сервер остаётся чистым.
Fresh не навязывает JS-анимации. Большинство эффектов реализуются через CSS, а Preact используется только для переключения состояний.
Поддерживаются библиотеки:
import autoAnimate from "@formkit/auto-animate";
import { useEffect, useRef } from "preact/hooks";
export function List({ items }) {
const ref = useRef(null);
useEffect(() => {
if (ref.current) autoAnimate(ref.current);
}, []);
return <ul ref={ref}>{items.map(i => <li>{i}</li>)}</ul>;
}
Важно учитывать:
window, document,
localStorage — только внутри эффектов,Использование Preact-библиотек требует понимания, где заканчивается сервер и начинается клиент.
Fresh автоматически:
Для Preact-библиотек рекомендуется:
Типичная схема:
Такой подход сохраняет производительность, масштабируемость и простоту поддержки кода во Fresh.