JavaScript Power Tools Part II: Composition Patterns in redux-sagaJavaScriptPowerToolsPartII:CompositionPatternsinredux-sagaJavaScriptPowerToolsPartII:CompositionPatternsinredux-saga
Matt Hink
17 May 2017
Share
In the last article, we investigated redux-saga's approach to the Saga pattern
in JavaScript. Specifically, the concept of a generator function that yields
'command objects', which describe the effects we want to happen, and rely on an
external driver to actually perform those effects. We observed that such a
generator function is called a 'saga'.
Now that we understand the mechanism of action, we can start looking at some of
the ways redux-saga allows us to compose sagas.
When I say, "compose sagas," I’m referring to the different ways to start a saga from
within another one. Why do I say "start" instead of "call"? Because sagas start
making much more sense when you think of them as subprograms rather than as
fancy patterns for constructing functions.
First, let's go back to what we learned in the last article - we’ll begin by invoking an async function from within a saga.
function*serverHello(name){const response =yieldcall(fetch,'example.com',{method:'POST',body: name });const text =yieldcall([response, response.text]);return text;}function*rootSaga(){const result =yieldcall(serverHello,"world");yieldcall(console.log, result);}
js
In serverHello, we're making two asynchronous function calls: the first
invokes fetch and waits for a response to become available, and the second
unwraps the response body text. In each case, we say that serverHello is
blocking on the result of the asynchronous function.
By the same token, rootSaga is blocking on the result of another saga, serverHello. It won't continue executing until the return value of
serverHello is available.
In some cases, this is a good thing. If we need to make two API calls, and the
order matters, this forces them to occur in a specific order.
function*rootSaga(){// Blocked until first serverHello finishesconst result0 =yieldcall(serverHello,'world');// blocked until second serverHello finishesconst result1 =yieldcall(serverHello,'Matt');yieldcall(console.log, result0);yieldcall(console.log, result1);}
js
However, in many cases, rootSaga might not need the result right away and
could be doing other work in the meantime.
function*rootSaga(){// Returns immediately with a Task objectconst task =yieldspawn(serverHello,'world');// Perform an effect in the meantimeyieldcall(console.log,"waiting on server result...");// Block on the result of serverHelloconst result =yieldjoin(task);// Use the result of serverHelloyieldcall(console.log, result);}
js
Here, we use the non-blockingspawn effect to tell redux-saga
that it should start the child saga, but resume rootSaga immediately. In this
case, the return value of the spawn effect is not the result of
serverHello, but rather a "Task object" which acts as a handle to
serverHello. rootSaga is free to continue execution until we decide we
actually need a result. At this point, we yield join with the Task object,
which instructs redux-saga to wait for the associated saga (serverHello) to
finish before resuming rootSaga. The result of serverHello then becomes the
return value of join.
With these two primitives, we can reconstruct our previous blocking calls.
We begin by starting both of our child sagas and saving a reference to each one's Task object.
Then, by yielding a join effect with multiple Task objects, we wait for both
to complete before resuming. The return value of join will become an array
containing each child saga's result.
But what if we don't care about the return value of those child sagas? For
instance, what if we only care that a POST to the server finished?
In this case, the join effect starts to look like an afterthought,
that will trip us up if we don't remember to include it.
In many cases, we'll want a saga to kick off a bunch of non-blocking
effects and then wait for them to finish before returning. In this case,
we'd use the fork effect, which creates an attached Task rather than an
unattached Task. The difference:
A parent saga that forks a child saga will wait for its child to complete before completing itself.
If a parent saga is cancelled before its child saga finishes executing, the child saga will be cancelled as well.
This example is better, but it still seems a bit... magical. After all, we're relying on
the implicit behavior of the fork to make sure both child sagas complete before finishing the execution itself. Moreover, starting up child sagas in parallel is a common pattern, so writing an entire saga to do it seems excessive. Because of this,
redux-saga provides the all effect, which takes an array of blocking
effects and waits for all of them to complete before resuming with all results.
To bring things full circle, this is more-or-less equivalent to
Promise.all.
Now that we have a frame of reference for thinking about concurrency in redux-saga, we can take a look at the kinds of things we can build, which will be the topic of the next article in this series.