От Redux Saga
Саги были ответом на “Redux thunks не композируются для сложных потоков” — генераторные функции, оркеструющие эффекты через рантайм. Триггеры заменяют слой оркестрации; остальная часть твоего Redux-стора может остаться на месте.
Поставляемого codemod-скрипта пока нет. migrate-from-saga намечен в roadmap на 1.0 — а до того момента эта страница является руководством по ручной конверсии.
Соответствие ментальных моделей
Заголовок раздела «Соответствие ментальных моделей»| redux-saga | Triggery |
|---|---|
takeLatest(type, worker) | concurrency: 'take-latest' (по умолчанию) |
takeEvery(type, worker) | concurrency: 'take-every' |
takeLeading(type, worker) | concurrency: 'take-first' |
throttle(ms, type, worker) | actions.throttle(ms).foo() для выходов или внешний throttling |
debounce(ms, type, worker) | actions.debounce(ms).foo() |
function* watcher() { yield takeLatest(…) } | createTrigger({ events: [type], concurrency, handler }) |
yield put(action) | actions.someAction?.(payload) |
yield select(selector) | conditions.someName — регистрируется через useReduxCondition |
yield call(api, …) | Просто await api(…) в async handler |
yield race({ task, timeout }) | Promise.race, с signal, кооперирующим отмену |
yield all([…]) | Promise.all(…) внутри обработчика |
cancel(task) | Автоматически под take-latest — signal.aborted переключается |
Паттерн 1 — watcher с takeLatest
Заголовок раздела «Паттерн 1 — watcher с takeLatest»function* loadDetailsSaga(action) {
const settings = yield select(state => state.settings);
if (!settings.notifications) return;
const data = yield call(api.fetchDetails, action.payload.id);
yield put(detailsLoaded(data));
}
function* rootSaga() {
yield takeLatest('chat/openConversation', loadDetailsSaga);
}export const detailsTrigger = createTrigger<{
events: { 'chat/openConversation': { id: string } };
conditions: { settings: Settings };
actions: { storeDetails: Details };
}>({
id: 'details-loader',
events: ['chat/openConversation'],
required: ['settings'],
concurrency: 'take-latest', // default; here for clarity
async handler({ event, signal, check, actions }) {
if (!check.is('settings', s => s.notifications)) return;
const data = await api.fetchDetails(event.payload.id, { signal });
if (signal.aborted) return;
actions.storeDetails?.(data);
},
});yield select становится условием; yield put становится действием; неявная отмена takeLatest становится явным signal — но с той же семантикой по умолчанию.
Паттерн 2 — takeEvery vs take-every
Заголовок раздела «Паттерн 2 — takeEvery vs take-every»yield takeEvery('analytics/track', function* (action) {
yield call(analytics.send, action.payload);
});createTrigger<{ events: { 'analytics/track': Event } }>({
id: 'analytics',
events: ['analytics/track'],
concurrency: 'take-every',
async handler({ event, signal }) {
await analytics.send(event.payload, { signal });
},
});take-every запускается конкурентно, как takeEvery. Ни один из запусков не прерывает другие.
Паттерн 3 — race / all
Заголовок раздела «Паттерн 3 — race / all»race и all не имеют прямого примитива в Triggery — их пишешь в обработчике стандартными Promise.race / Promise.all. signal служит шиной отмены для обоих:
async handler({ event, signal, actions }) {
const result = await Promise.race([
api.fetchSlow(event.payload, { signal }),
new Promise<'timeout'>(r => setTimeout(() => r('timeout'), 5000)),
]);
if (signal.aborted) return;
if (result === 'timeout') actions.showError?.('timed out');
else actions.storeResult?.(result);
}Паттерн 4 — саги с channel / event emitter
Заголовок раздела «Паттерн 4 — саги с channel / event emitter»Саги, оборачивающие не-Redux источник событий (WebSocket, кастомный emitter) через eventChannel, чисто ложатся в триггеры + адаптер-продюсер:
function* watchSocket() {
const channel = yield call(createSocketChannel, socket);
while (true) {
const msg = yield take(channel);
yield call(handle, msg);
}
}// in a component:
const fireMessage = useEvent(messageTrigger, 'new-message');
useSocketIoEvent(socket, 'new-message', fireMessage);@triggery/socket убирает шаблонный код с каналом.
Когда саги можно оставить
Заголовок раздела «Когда саги можно оставить»Генераторы — это до сих пор самый чистый способ выразить долгоживущие многошаговые потоки с явными точками отмены (yield call, yield take, yield cancel). Если у тебя есть сага, напоминающая конечный автомат, рассмотри перенос FSM в XState и запуск её из обработчика Triggery — см. сравнение с XState.
Статус codemod-скрипта
Заголовок раздела «Статус codemod-скрипта»Codemod-скрипта migrate-from-saga пока нет — саги переносятся вручную по соответствию выше. Codemod-скрипты extract-trigger и migrate-from-listener-middleware, которые поставляются уже сейчас, покрывают пути useEffect и RTK listenerMiddleware. Поддержка саг отслеживается в GitHub Project board.
Даже когда codemod появится, он будет трогать только простые формы watcher-ов takeLatest / takeEvery / takeLeading — генераторы с несколькими вызовами yield take всегда требуют человеческой ревизии.