core/selector-channels
Use takeLatestFromSelector, takeEveryFromSelector, or takeLeadingFromSelector for saga reactions to selector changes. Reserve createChannelFromSelector for custom race/condition/timeout flows; close r
SKILL.mdskills/core/selector-channels/SKILL.md
GitHubSelector Channels — agent implementation rules
Use this skill when a saga should react to selector value changes instead of action dispatches. Keep detailed examples in @themislib/themis/docs/SAGAS.md; this skill is for quick implementation decisions and guardrails.
Canonical references
- Human guide:
@themislib/themis/docs/SAGAS.md#selector-channel-effects - Public API:
@themislib/themis/utils/sagas/selector-channel-effectsor aggregate@themislib/themis/saga. - Related skills:
core/sagas,core/wait-for,core/channel-effects, plus the selected Store family selector lifecycle skill when direct selector call modes matter.
Choose the helper
takeLatestFromSelector— default for load/search/recompute-on-change; cancels previous worker.takeEveryFromSelector— every selector emission must be handled independently.takeLeadingFromSelector— ignore new emissions while current work runs.createChannelFromSelector— only for customrace, conditional loops, manual cancellation, or timeout flows.waitFor— one-shot wait before continuing, not a continuous watcher.
Do
- Use named selectors created by the selected Store family utilities; selector-channel helpers require the shared
.select(state, ...args)/.effect(...args)read shape. - Import the selectors from the owning slice's
[slice]-selectors.tsfile; do not declare localselect*functions/factories inside saga modules. - Pass plain stable selector arguments as the args tuple in the second position, e.g.
[itemId], matching.select(state, ...args)/.effect(...args)rather than direct-call reactive argument wrappers. - Prefer primitive scalar args in selector-channel tuples; pass object/function args only when their identity is stable and intentional.
- Expect worker payloads to include
{ payload, prevPayload }. - Prefer the
take*FromSelectorhelpers because they create and clean up channels for you. - Close raw channels in
finallyevery time. - Keep selector-channel workers focused and cancellation-safe.
Don't
- Do not wrap selector channels in generic
channel-effects; selector helpers already own the lifecycle. - Do not use closures or inline selector lambdas to smuggle arguments.
- Do not declare module-local
select*selectors inside the saga file to feed selector-channel helpers; move them to[slice]-selectors.tsand import them. - Do not fall back to
take('*')or other wildcard takes when a selector-channel helper or concrete action creator would model the intent. - Do not choose a raw channel for simple load-on-change behavior.
- Do not leave manual channel loops without cancellation and cleanup.
- Do not confuse selector channels with action watchers; use core saga effects for actions.
- Do not pass or subscribe to direct reactive/observable selector outputs in selector-channel effects.
- Do not put fresh object, array, or function literals in selector-channel args tuples; split to scalar args or create a stable reference before the helper call.
Implementation cues
- Selector-channel effects use the saga-context Redux store directly: read with
getState()and subscribe withsubscribe(). Do not introduce family-specific reactive wrappers for selector channels. - For no-arg selectors, pass the worker directly to the helper.
- For selectors with args, pass the args tuple before the worker.
- The args tuple may be a fresh array wrapper, but the values inside it should be
stable selector args; avoid
[ { id } ],[ids.map(...)], or[() => ...]unless the inner value is an intentional stable reference. - The first channel emission can have
prevPayloadasnull/undefined; guard transition logic accordingly. - If multiple dispatches produce intermediate values, model the final UI value with selectors or action design; there is no public lock/unlock action API.
- For generic IPC/websocket/DOM
EventChannelconsumers, route tocore/channel-effectsinstead.
Examples
1. Use takeLatestFromSelector for no-arg selector changes
import { put } from "typed-redux-saga";
import { takeLatestFromSelector } from "@themislib/themis/saga";
function* watchCurrentItem() {
yield* takeLatestFromSelector(selectCurrentItemId, function* ({ payload: itemId }) {
if (itemId) yield* put(loadItem(itemId));
});
}
2. Pass selector arguments as an args tuple
import { call } from "typed-redux-saga";
import { takeEveryFromSelector } from "@themislib/themis/saga";
function* watchTodo(todoId: string) {
yield* takeEveryFromSelector(selectTodoById, [todoId], function* ({ payload: todo }) {
if (todo) yield* call(syncTodo, todo);
});
}
Use scalar args like todoId. Do not pass freshly built selector option objects
such as [{ id: todoId }]; if a selector intentionally keys by object identity,
construct that object once and reuse the stable reference.
3. Use takeLeadingFromSelector when in-flight work should ignore new values
import { takeLeadingFromSelector } from "@themislib/themis/saga";
function* watchCheckoutReadiness() {
yield* takeLeadingFromSelector(selectCheckoutReady, function* ({ payload: ready }) {
if (ready) yield* submitCheckoutWorker();
});
}
4. Read payload and prevPayload when transition semantics matter
import { put } from "typed-redux-saga";
import { takeEveryFromSelector } from "@themislib/themis/saga";
function* watchConnectionStatus() {
yield* takeEveryFromSelector(selectConnectionStatus, function* ({ payload, prevPayload }) {
if (prevPayload === "connecting" && payload === "connected") {
yield* put(connectionEstablished());
}
});
}
5. Use createChannelFromSelector only for custom race/timeout flows
import { call, delay, race, take } from "typed-redux-saga";
import { createChannelFromSelector } from "@themislib/themis/saga";
function* waitForFirstReadyItem(itemId: string) {
const channel = yield* createChannelFromSelector(selectTodoById, itemId);
try {
return yield* race({ item: take(channel), timeout: delay(5_000) });
} finally {
channel.close();
}
}
6. ❌ Bad: wrapping auto-forking helpers or hiding args in closures
// BAD: the helper already forks and owns channel cleanup; closure args bypass the public args tuple.
function* watchTodoBad(todoId: string) {
yield* fork(takeLatestFromSelector, () => selectTodoById.select(store.state, todoId), syncTodoWorker);
}
function* watchTodoGood(todoId: string) {
yield* takeLatestFromSelector(selectTodoById, [todoId], syncTodoWorker);
}
Verification cues
- Test that the selected helper matches desired cancellation behavior (
latest,every, orleading). - For raw channel use, test cancellation/timeout paths and assert cleanup where practical.
- Search for accidental duplicate selector watchers if adding a new watcher for existing state.
- Run the smallest relevant saga tests when docs/skills examples change.
Common mistakes to prevent
- Hand-rolled channel loops where
takeLatestFromSelectoris enough. - Missing
channel.close()infinally. - Passing selector args through closures instead of an args tuple.
- Passing freshly constructed object/array/function selector args in the tuple.
- Using generic
takeLatest(channel, worker)on a channel created from a selector instead oftakeLatestFromSelector. - Declaring
select*selectors locally in the saga module instead of importing them from[slice]-selectors.ts. - Reaching for
take('*')instead of a selector-channel helper or a concrete action creator.
See also
@themislib/themis/docs/SAGAS.md— full selector-channel examples and saga context.core/wait-for— one-shot selector waits.core/channel-effects— genericEventChannelconsumers.- Selected Store family selector lifecycle skill — selector initialization and call modes.