From RTK listenerMiddleware
listenerMiddleware from @reduxjs/toolkit is the closest thing in the Redux ecosystem to a trigger. The migration is mostly mechanical: one startListening({ actionCreator, effect }) registration becomes one createTrigger({ id, events, handler }). The codemod migrate-from-listener-middleware handles the actionCreator shape; other shapes need human review.
Mental model mapping
Section titled “Mental model mapping”| listenerMiddleware | Triggery |
|---|---|
startListening({ actionCreator: foo, effect }) | createTrigger({ events: ['foo'], handler }) |
startListening({ matcher: isAnyOf(a, b) }) | events: ['a', 'b'] and branch on event.name |
startListening({ predicate: (action, state) => … }) | Predicate → condition + handler guard |
effect(action, api) — api.getState() | conditions.someName (registered via useReduxCondition) |
api.dispatch(action) | actions.someAction?.(payload) |
api.cancelActiveListeners() | concurrency: 'take-latest' (default) with signal |
api.signal | signal in handler context |
api.fork(task) | Sub-handler; explicit take-every concurrency |
Pattern 1 — actionCreator-based listener
Section titled “Pattern 1 — actionCreator-based listener”State access (api.getState().settings) becomes a useReduxCondition registered by the settings feature; the toast dispatch becomes a useAction on the UI feature.
Pattern 2 — matcher
Section titled “Pattern 2 — matcher”Listing both names in events: indexes the trigger under both event keys — the handler sees a discriminated union of payloads.
Pattern 3 — predicate against state
Section titled “Pattern 3 — predicate against state”The predicate’s “is this state right?” check turns into check.is inside the handler — same logic, lazily evaluated from a single source.
Pattern 4 — race against signal (async listeners)
Section titled “Pattern 4 — race against signal (async listeners)”take-latest is the default; you only pass concurrency: to opt into something else.
Codemod
Section titled “Codemod”@triggery/codemod ships migrate-from-listener-middleware for the actionCreator shape:
For each startListening({ actionCreator, effect }) call the codemod generates one *.trigger.ts stub with the effect body dropped verbatim into the handler and a // TODO marker for the getState()/dispatch() conversion. Matcher / predicate shapes are detected but skipped — you’ll do those by hand from the patterns above.
Keeping Redux around
Section titled “Keeping Redux around”You don’t have to remove Redux. The most common path is: keep the store, keep createSlice, and replace listenerMiddleware as a side-effect mechanism with triggers. The @triggery/redux adapter exposes selectors as conditions in 30 lines — see @triggery/redux.