Интеграция с Next.js
Next.js совмещает серверный рендеринг, React Server Components и клиентскую навигацию. Для lite-fsm это означает: менеджер нельзя делать общим для всех запросов, а серверный снимок нужно передавать в клиентское дерево через механизм гидратации.
Требования интеграции
При использовании менеджера состояния с Next.js учитывайте:
-
Создание независимого стора для каждого запроса: Сервер Next.js обрабатывает множество запросов одновременно, поэтому хранилище должно создаваться для каждого запроса отдельно, а не использоваться как глобальная переменная.
-
Гидратация данных при SSR: Next.js рендерит приложение на сервере и затем гидратирует его на клиенте. Данные в хранилище должны быть согласованы, иначе возникнет ошибка гидратации.
-
Поддержка SPA-маршрутизации: Next.js поддерживает гибридную модель клиентской маршрутизации, поэтому данные, привязанные к маршруту, нужно обновлять или сбрасывать при переходах между страницами.
-
Совместимость с кешированием App Router: серверные данные и изменяемое клиентское состояние должны храниться отдельно.
Рекомендации по использованию
Для работы с Next.js App Router рекомендуется:
- Не используйте глобальный manager на сервере — создавайте manager для каждого запроса или сценария.
- React Server Components не должны читать или писать в клиентский manager — RSC не используют клиентские хуки и контекст.
- Держите в
lite-fsmтолько изменяемое клиентское состояние — серверные данные Next.js, search params и route params не нужно дублировать в менеджере без отдельного требования.
Структура проекта
Структура проекта с Next.js App Router может выглядеть так:
/app
/components
ProductDetails.tsx
/api
route.ts
layout.tsx
page.tsx
StoreProvider.tsx
/src
/store
/machines
counter.ts
profile.ts
root.ts
create-machine.ts
hooks.ts
index.ts
types.ts
next.config.js
package.jsonНастройка хранилища
Вместо глобального store создайте функцию makeStore, которая возвращает новый MachineManager:
// src/store/index.ts
import { MachineManager, type MachinesState } from "@lite-fsm/core";
import { devToolsMiddleware } from "@lite-fsm/middleware/devTools";
import { immerMiddleware } from "@lite-fsm/middleware/immer";
import { counter } from "./machines/counter";
import { profile } from "./machines/profile";
import { root } from "./machines/root";
import type { AppEvents, Dependencies } from "./types";
const cfg = { root, counter, profile };
export type FSMConfigType = typeof cfg;
export type AppState = MachinesState<FSMConfigType>;
export const makeStore = () => {
const manager = MachineManager<FSMConfigType, AppEvents>(cfg, {
middleware: [immerMiddleware, devToolsMiddleware({ blacklistActions: [] })],
onError: console.error,
});
manager.setDependencies({
getState: manager.getState,
playerService: {
play: () => undefined,
pause: () => undefined,
},
});
return manager;
};
export type AppStore = ReturnType<typeof makeStore>;Предоставление хранилища компонентам
Создайте client provider для Next.js:
// app/StoreProvider.tsx
"use client";
import { useRef, type ReactNode } from "react";
import { FSMContextProvider } from "@lite-fsm/react";
import { makeStore } from "../src/store";
import type { AppStore } from "../src/store";
export default function StoreProvider({ children }: { children: ReactNode }) {
const storeRef = useRef<AppStore | null>(null);
if (!storeRef.current) {
storeRef.current = makeStore();
}
return <FSMContextProvider machineManager={storeRef.current}>{children}</FSMContextProvider>;
}Persist в App Router
Persist можно создавать вместе с менеджером без ручного typeof window: createJsonStorage принимает отложенную функцию хранилища и не вызывает её во время создания адаптера или серверного предварительного рендеринга. FSMContextProvider запускает жизненный цикл persist только после монтирования.
// src/store/persist.ts
import { createJsonStorage, persistManager } from "@lite-fsm/persist";
import { makeStore, type FSMConfigType } from ".";
export const makePersistentStore = () => {
const manager = makeStore();
const persist = persistManager(manager, {
storage: createJsonStorage<FSMConfigType>({
key: "app:state:v1",
storage: () => window.localStorage,
}),
machines: ["profile"],
throttleMs: 300,
});
return { manager, persist };
};
export type PersistentStore = ReturnType<typeof makePersistentStore>;// app/StoreProvider.tsx
"use client";
import { useRef, type ReactNode } from "react";
import { FSMContextProvider } from "@lite-fsm/react";
import { makePersistentStore, type PersistentStore } from "../src/store/persist";
export default function StoreProvider({ children }: { children: ReactNode }) {
const storeRef = useRef<PersistentStore | null>(null);
if (!storeRef.current) {
storeRef.current = makePersistentStore();
}
return (
<FSMContextProvider machineManager={storeRef.current.manager} persist={[storeRef.current.persist]}>
{children}
</FSMContextProvider>
);
}useIsPersistRestoring() проверяет только фазу "restoring" среди статусов, отличных от null. Если экран должен скрывать данные до завершения первого восстановления, используйте status?.phase === "idle" || status?.phase === "restoring" для нужного элемента из usePersistStatuses().
Использование хуков
Создайте типизированные версии хуков:
// src/store/hooks.ts
import type { TypedUseManagerHook, TypedUseSelectorHook, TypedUseTransitionHook } from "@lite-fsm/react";
import {
useManager as _useManager,
useSelector as _useSelector,
useTransition as _useTransition,
} from "@lite-fsm/react";
import type { FSMConfigType } from ".";
import type { AppEvents } from "./types";
export const useTransition: TypedUseTransitionHook<AppEvents> = _useTransition;
export const useSelector: TypedUseSelectorHook<FSMConfigType> = _useSelector;
export const useManager: TypedUseManagerHook<FSMConfigType, AppEvents> = _useManager;Интеграция с приложением
В App Router provider обычно размещают внутри body, чтобы корневой html оставался серверной частью layout:
// app/layout.tsx
import StoreProvider from "./StoreProvider";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ru">
<body>
<StoreProvider>{children}</StoreProvider>
</body>
</html>
);
}Гидратация состояния с сервера
Когда серверу нужно подготовить состояние и доставить его клиенту, передайте результат manager.dehydrate() в клиентский компонент-обёртку на базе FSMHydrationBoundary. Компонент рассчитывает состояние после гидратации во время отрисовки, поэтому серверная разметка и первая клиентская отрисовка читают один и тот же snapshot через useSelector().
useHydrateSnapshot() применяет снимок только в useLayoutEffect. Используйте его для фонового восстановления после монтирования; он не устраняет SSR hydration mismatch на первой отрисовке. Подробности — в разделе Сохранение состояния.
// app/page.tsx — Server Component
import { makeStore } from "../src/store";
import { StoreSnapshotBoundary } from "./StoreSnapshotBoundary";
export default async function Page() {
const manager = makeStore();
manager.transition({ type: "BOOTSTRAP", payload: await loadInitialData() });
return (
<StoreSnapshotBoundary snapshot={manager.dehydrate()}>
<PageContent />
</StoreSnapshotBoundary>
);
}// app/StoreSnapshotBoundary.tsx
"use client";
import { type ReactNode } from "react";
import type { MachineManagerSnapshot } from "@lite-fsm/core";
import { FSMHydrationBoundary } from "@lite-fsm/react";
import type { FSMConfigType } from "../src/store";
type Props = {
children: ReactNode;
snapshot: MachineManagerSnapshot<FSMConfigType>;
};
export function StoreSnapshotBoundary({ children, snapshot }: Props) {
return (
<FSMHydrationBoundary snapshot={snapshot} strategy="merge">
{children}
</FSMHydrationBoundary>
);
}Если StoreProvider подключен не в корневом layout, разместите StoreSnapshotBoundary внутри того же провайдера: FSMHydrationBoundary получает manager из React-контекста.
Рекомендации по архитектуре
App Router отличается от традиционной SPA-архитектуры. При интеграции lite-fsm с Next.js разделяйте данные по месту хранения и жизненному циклу:
- Используйте lite-fsm для глобальных изменяемых данных и процессов с явными состояниями.
- Оставляйте route/search params в Next.js, а локальное состояние форм — рядом с компонентами, если оно не нужно другим частям приложения.
- Разделяйте серверные и клиентские компоненты — серверные компоненты не используют React-контекст или хуки
@lite-fsm/react.
Проверка работоспособности
Проверьте следующие аспекты интеграции:
- Серверный рендеринг — проверьте HTML-вывод сервера и первую клиентскую отрисовку.
- Изоляция менеджера — убедитесь, что на сервере создаётся отдельный менеджер для каждого запроса.
- Изменение маршрута — проверьте переходы между страницами и повторную инициализацию route-specific данных.
- Мутации и кеширование — проверьте сценарий: мутация, переход на другой маршрут, возврат на исходный маршрут.