Concept
take(actionType), covered in Effects, pauses a saga until a matching Redux action is dispatched. Channels generalize this same pause-until-an-event mechanism to work with events from outside Redux entirely, WebSocket messages, DOM events, timers, any callback-based API, letting a saga yield take(channel) the exact same way it would yield take("SOME_ACTION").
import { eventChannel } from "redux-saga";
import { call, take, put } from "redux-saga/effects";
function createSocketChannel(socket) {
return eventChannel((emit) => {
const handler = (message) => emit(message); // call `emit` whenever a real event happens
socket.on("message", handler);
return () => socket.off("message", handler); // UNSUBSCRIBE function, called when the channel closes
});
}eventChannel's single argument is a "subscriber" function: it receives an emit callback, wires that callback into whatever real external event source you're bridging (here, socket.on(...)), and returns an unsubscribe function, called automatically when the channel is closed, ensuring no lingering event listeners after a saga stops watching it.
function* fetchUserSaga(action) {const user = yield call(api.getUser, action.payload);yield put({ type: 'USER_LOADED', payload: user });}store.dispatch({ type: 'FETCH_USER', payload: 1 });
The saga middleware watches for matching actions, takeEvery starts a new run of fetchUserSaga each time FETCH_USER is dispatched.
Confirmed: yield take(channel) receives external events, in order, one per resume
function* watchSocketMessages(socket) {
const channel = yield call(createSocketChannel, socket);
while (true) {
const message = yield take(channel); // pauses until the NEXT event arrives
yield put({ type: "MESSAGE_RECEIVED", payload: message });
}
}Confirmed by wiring a real Node EventEmitter through eventChannel and driving a saga with while (true) { yield take(channel); ... }: three emitted events ("tick" with values 1, 2, 3, emitted with real delays between them) were received by three sequential take(channel) calls, in the exact order emitted, the generator genuinely paused after each take, resuming only when the next real event fired, exactly mirroring take(actionType)'s behavior with dispatched actions but driven by an arbitrary external emitter instead of the Redux store.
Why this matters: a uniform model for "wait for the next thing to happen"
Without channels, integrating a WebSocket or similar external event source into a saga would require manually bridging it, typically dispatching a Redux action from inside the socket's own callback, then having a saga take() that action. Channels let you skip the intermediate action entirely for cases where the event doesn't need to exist as a first-class Redux action at all, or, more precisely, let the saga consume the raw event stream directly, dispatching Redux actions only for the processed results the rest of the app actually needs to react to (as shown above: only MESSAGE_RECEIVED is dispatched, not one action per raw socket event).
Multiple concurrent workflows: channels as a coordination primitive
function* rootSaga() {
const channel = yield call(createSocketChannel, socket);
yield fork(watchMessages, channel); // one saga consumes from the channel
yield fork(watchConnectionStatus, channel); // another can watch a DIFFERENT emitted event type from the SAME channel
}Because a channel is just a value (the object eventChannel(...) returns), it can be passed to multiple sagas, forked into concurrent workers, or closed explicitly (channel.close()) to unsubscribe and signal any sagas still take-ing from it that no more values are coming, genuinely useful for coordinating complex, multi-part workflows built from more than one independent event source.
Try It
Predict the outcome before checking the solution.
const emitter = new EventEmitter();
const channel = eventChannel((emit) => {
emitter.on("data", emit);
return () => emitter.off("data", emit);
});