Skip to Content
👋 Добро пожаловать в документацию lite-fsm!
ПрактикаИнтеграция с Next.js

Интеграция с Next.js

Next.js совмещает серверный рендеринг, React Server Components и клиентскую навигацию. Для lite-fsm это означает: менеджер нельзя делать общим для всех запросов, а серверный снимок нужно передавать в клиентское дерево через механизм гидратации.

Требования интеграции

При использовании менеджера состояния с Next.js учитывайте:

  1. Создание независимого стора для каждого запроса: Сервер Next.js обрабатывает множество запросов одновременно, поэтому хранилище должно создаваться для каждого запроса отдельно, а не использоваться как глобальная переменная.

  2. Гидратация данных при SSR: Next.js рендерит приложение на сервере и затем гидратирует его на клиенте. Данные в хранилище должны быть согласованы, иначе возникнет ошибка гидратации.

  3. Поддержка SPA-маршрутизации: Next.js поддерживает гибридную модель клиентской маршрутизации, поэтому данные, привязанные к маршруту, нужно обновлять или сбрасывать при переходах между страницами.

  4. Совместимость с кешированием 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.

Проверка работоспособности

Проверьте следующие аспекты интеграции:

  1. Серверный рендеринг — проверьте HTML-вывод сервера и первую клиентскую отрисовку.
  2. Изоляция менеджера — убедитесь, что на сервере создаётся отдельный менеджер для каждого запроса.
  3. Изменение маршрута — проверьте переходы между страницами и повторную инициализацию route-specific данных.
  4. Мутации и кеширование — проверьте сценарий: мутация, переход на другой маршрут, возврат на исходный маршрут.
Last updated on