Movatterモバイル変換


[0]ホーム

URL:


Skip to main content
🇺🇦 STOP WAR IN UKRAINE 🇺🇦

Declarative Effects

Inredux-saga, Sagas are implemented using Generator functions. To express the Saga logic, we yield plain JavaScript Objects from the Generator. We call those ObjectsEffects. An Effect is an object that contains some information to be interpreted by the middleware. You can view Effects like instructions to the middleware to perform some operation (e.g., invoke some asynchronous function, dispatch an action to the store, etc.).

To create Effects, you use the functions provided by the library in theredux-saga/effects package.

In this section and the following, we will introduce some basic Effects. And see how the concept allows the Sagas to be easily tested.

Sagas can yield Effects in multiple forms. The easiest way is to yield a Promise.

For example suppose we have a Saga that watches aPRODUCTS_REQUESTED action. On each matching action, it starts a task to fetch a list of products from a server.

import{ takeEvery}from'redux-saga/effects'
importApifrom'./path/to/api'

function*watchFetchProducts(){
yieldtakeEvery('PRODUCTS_REQUESTED', fetchProducts)
}

function*fetchProducts(){
const products=yieldApi.fetch('/products')
console.log(products)
}

In the example above, we are invokingApi.fetch directly from inside the Generator (In Generator functions, any expression at the right ofyield is evaluated then the result is yielded to the caller).

Api.fetch('/products') triggers an AJAX request and returns a Promise that will resolve with the resolved response, the AJAX request will be executed immediately. Simple and idiomatic, but...

Suppose we want to test the generator above:

const iterator=fetchProducts()
assert.deepEqual(iterator.next().value,??)// what do we expect ?

We want to check the result of the first value yielded by the generator. In our case it's the result of runningApi.fetch('/products') which is a Promise . Executing the real service during tests is neither a viable nor practical approach, so we have tomock theApi.fetch function, i.e. we'll have to replace the real function with a fake one which doesn't actually run the AJAX request but only checks that we've calledApi.fetch with the right arguments ('/products' in our case).

Mocks make testing more difficult and less reliable. On the other hand, functions that return values are easier to test, since we can use a simpleequal() to check the result. This is the way to write the most reliable tests.

Not convinced? I encourage you to readEric Elliott's article:

(...)equal(), by nature answers the two most important questions every unit test must answer,but most don’t:

  • What is the actual output?
  • What is the expected output?

If you finish a test without answering those two questions, you don’t have a real unit test. You have a sloppy, half-baked test.

What we actually need to do is make sure thefetchProducts task yields a call with the right function and the right arguments.

Instead of invoking the asynchronous function directly from inside the Generator,we can yield only a description of the function invocation. i.e. We'll yield an object which looks like

// Effect -> call the function Api.fetch with `./products` as argument
{
CALL:{
fn:Api.fetch,
args:['./products']
}
}

Put another way, the Generator will yield plain Objects containinginstructions, and theredux-saga middleware will take care of executing those instructions and giving back the result of their execution to the Generator. This way, when testing the Generator, all we need to do is to check that it yields the expected instruction by doing a simpledeepEqual on the yielded Object.

For this reason, the library provides a different way to perform asynchronous calls.

import{ call}from'redux-saga/effects'

function*fetchProducts(){
const products=yieldcall(Api.fetch,'/products')
// ...
}

We're using now thecall(fn, ...args) function.The difference from the preceding example is that now we're not executing the fetch call immediately, instead,call creates a description of the effect. Just as in Redux you use action creators to create a plain object describing the action that will get executed by the Store,call creates a plain object describing the function call. The redux-saga middleware takes care of executing the function call and resuming the generator with the resolved response.

This allows us to easily test the Generator outside the Redux environment. Becausecall is just a function which returns a plain Object.

import{ call}from'redux-saga/effects'
importApifrom'...'

const iterator=fetchProducts()

// expects a call instruction
assert.deepEqual(
iterator.next().value,
call(Api.fetch,'/products'),
"fetchProducts should yield an Effect call(Api.fetch, './products')"
)

Now we don't need to mock anything, and a basic equality test will suffice.

The advantage of thosedeclarative calls is that we can test all the logic inside a Saga by iterating over the Generator and doing adeepEqual test on the values yielded successively. This is a real benefit, as your complex asynchronous operations are no longer black boxes, and you can test in detail their operational logic no matter how complex it is.

call also supports invoking object methods, you can provide athis context to the invoked functions using the following form:

yieldcall([obj, obj.method], arg1, arg2,...)// as if we did obj.method(arg1, arg2 ...)

apply is an alias for the method invocation form

yieldapply(obj, obj.method,[arg1, arg2,...])

call andapply are well suited for functions that return Promise results. Another functioncps can be used to handle Node style functions (e.g.fn(...args, callback) wherecallback is of the form(error, result) => ()).cps stands for Continuation Passing Style.

For example:

import{ cps}from'redux-saga/effects'

const content=yieldcps(readFile,'/path/to/file')

And of course you can test it just like you testcall:

import{ cps}from'redux-saga/effects'

const iterator=fetchSaga()
assert.deepEqual(iterator.next().value,cps(readFile,'/path/to/file'))

cps also supports the same method invocation form ascall.

A full list of declarative effects can be found in theAPI reference.


[8]ページ先頭

©2009-2025 Movatter.jp