Skip to Content
👋 Добро пожаловать в документацию lite-fsm!
РуководствоПродвинутые концепцииПользовательская отмена

Ручная отмена эффектов

Помимо type: "latest" и автоматического игнорирования устаревших transition-вызовов, lite-fsm позволяет задать собственное правило блокировки через cancelFn. Используйте cancelFn, когда актуальность effect зависит не только от номера запуска.

Механизм ручной отмены

Ручная отмена не прерывает async-код сама по себе. Она задаёт predicate, который проверяется перед каждым transition внутри effect.

Пример использования

В следующем примере показано, как реализовать ручную отмену эффекта на основе изменения значения в контексте:

import { createEffect, createMachine } from "@lite-fsm/core"; export const streams = createMachine({ config: { IDLE: { DO_LOAD_STREAMS: "LOAD_STREAMS_PENDING", LOAD_STREAMS_RESOLVE: "IDLE", LOAD_STREAMS_REJECT: "IDLE", }, LOAD_STREAMS_PENDING: { LOAD_STREAMS_STARTED: "IDLE", }, }, initialState: "IDLE", initialContext: {}, effects: { LOAD_STREAMS_PENDING: createEffect({ type: "latest", // Функция отмены, которая проверяет условие перед каждым вызовом transition cancelFn: ({ getState }) => { const startSession = getState().player.context.session.sessionId; return () => { const currentSession = getState().player.context.session.sessionId; // Если сессия изменилась — transition-вызовы будут игнорироваться return startSession !== currentSession; }; }, effect: async ({ transition, api }) => { transition({ type: "LOAD_STREAMS_STARTED" }); try { // Сетевой запрос в любом случае выполнится до конца await api.loadStreams(); transition({ type: "LOAD_STREAMS_RESOLVE" }); } catch (error) { transition({ type: "LOAD_STREAMS_REJECT", payload: { error }, }); } }, }), }, });

Механизм работы отмены

Функция cancelFn возвращает проверку, которая вызывается перед каждым вызовом transition в эффекте:

  1. Инициализация: При первом запуске эффекта вызывается cancelFn, которая сохраняет начальное состояние и возвращает функцию проверки
  2. Выполнение эффекта продолжается: Важно понимать, что сам код эффекта продолжает выполняться даже при отмене
  3. Проверка перед transition: Перед каждым вызовом transition внутри эффекта, система вызывает функцию проверки отмены
  4. Игнорирование transition при отмене: Если функция проверки возвращает true, вызов transition игнорируется, и машина состояний не получает событие

Таким образом, cancelFn не прерывает выполнение effect, а блокирует его влияние на состояние машины.

Сложные сценарии отмены

Отмена по внешнему сигналу

import { MachineManager, createEffect, createMachine } from "@lite-fsm/core"; import { immerMiddleware } from "@lite-fsm/middleware/immer"; const createSearchController = () => ({ controller: new AbortController(), reset() { this.controller.abort(); this.controller = new AbortController(); return this.controller; }, }); export const search = createMachine({ config: { IDLE: { SEARCH: "SEARCH_PENDING", }, SEARCH_PENDING: { SEARCH_STARTED: "SEARCHING", }, SEARCHING: { SEARCH: "SEARCH_PENDING", SEARCH_RESOLVE: "RESULTS", SEARCH_REJECT: "ERROR", }, RESULTS: { SEARCH: "SEARCH_PENDING", }, ERROR: { SEARCH: "SEARCH_PENDING", }, }, initialState: "IDLE", initialContext: { query: "", results: null as unknown[] | null, error: null as Error | null, }, reducer: (state, action, { nextState }) => { state.state = nextState; switch (action.type) { case "SEARCH": state.context.query = action.payload.query; break; case "SEARCH_RESOLVE": state.context.results = action.payload.results; state.context.error = null; break; case "SEARCH_REJECT": state.context.error = action.payload.error; state.context.results = null; break; } }, effects: { SEARCH_PENDING: ({ transition }) => { transition({ type: "SEARCH_STARTED" }); }, SEARCHING: createEffect({ cancelFn: ({ searchController }) => { const controller = searchController.controller; return () => controller.signal.aborted; }, effect: async ({ transition, api, searchController, getState }) => { const { query } = getState().search.context; const controller = searchController.reset(); try { const results = await api.search(query, { signal: controller.signal }); transition({ type: "SEARCH_RESOLVE", payload: { results } }); } catch (err) { if (err instanceof Error && err.name === "AbortError") return; transition({ type: "SEARCH_REJECT", payload: { error: err as Error } }); } }, }), }, }); const manager = MachineManager({ search }, { middleware: [immerMiddleware] }); manager.setDependencies({ api: apiService, searchController: createSearchController(), getState: manager.getState, }); manager.transition({ type: "SEARCH", payload: { query: "первый" } }); // Предыдущий запрос будет автоматически отменён через AbortController manager.transition({ type: "SEARCH", payload: { query: "второй" } });

Лучшие практики

  1. Чётко определите условие блокировки: условие должно быть детерминированным и основанным на состоянии автомата или явной зависимости.
  2. Помните, что код эффекта продолжает выполняться: Отмена эффекта блокирует только вызовы transition, но сам код продолжает выполняться.
  3. Используйте AbortController для фактической отмены операций: поскольку код эффекта продолжает выполняться, используйте AbortController для отмены HTTP-запросов и других ресурсоёмких операций.
  4. Сочетайте блокировку transition и отмену операции: при необходимости комбинируйте cancelFn и AbortController.
  5. Избегайте побочных эффектов в функции проверки: функция, возвращаемая из cancelFn, должна проверять условие, а не изменять состояние.
  6. Документируйте условия отмены: Явно указывайте, при каких условиях будут игнорироваться transition-вызовы.
Last updated on