Testing Complex Asynchronous Redux Actions - javascript

So, let's say I have the next action:
export function login({ email, password, redirectTo, doNotRedirect }) {
return ({ dispatch }) => {
const getPromise = async () => {
const basicToken = Base64.encode(`${email}:${password}`);
const authHeaders = { Authorization: `Basic ${basicToken}` };
const { payload, error } = await dispatch(sendAuthentication(authHeaders));
if (error) throw payload;
const { username, token, fromTemporaryPassword } = payload;
const encodedToken = Base64.encode(`${username}:${token}`);
dispatch(persistence.set('authorizationToken', encodedToken));
dispatch(postGlobalId({ username }));
dispatch(setIsLoggedIn(true));
dispatch(setIsFromTemporaryPassword(fromTemporaryPassword));
await dispatch(clientActions.fetchClient);
if (doNotRedirect) return;
if (fromTemporaryPassword)
dispatch(updatePath('/profile/change-password'));
else
dispatch(updatePath(redirectTo || '/dashboard'));
};
return {
type: AUTHENTICATION_LOGIN,
payload: getPromise()
};
};
}
And I want to add tests for it, to add reliability to the code.
So, here are few things:
We send authentication headers and get data as a response
We throw an error if some error is present in the response
We set up all needed tokens, dispatch all needed actions to show that we are logged in now
Fetching client data
Based on params and received data, we redirect to needed route / don't redirect
The question is that it is really too hard to test and we need to stub literally everything, which is bad due to brittle tests, fragility and too much of implementation knowing (not to mention that it is pretty challenging to stub dispatch to work properly).
Therefore, should I test all of these 5 points, or to focus only on the most important stuff, like sending authorization request, throw error and check redirects? I mean, the problem with all flags that they can be changed, so it is not that reliable.
Another solution is just to separate these activities into something like following:
auth
setLoginInfo
handleRedirects
And to pass all needed functions to invoke through dependency injection (here just with params, basically)? With this approach I can spy only invoking of this functions, without going into much details.
I am quite comfortable with unit testing of pure functions and handling different edge-cases for them (without testing too much implementation, just the result), but testing complex functions with side-effects is really hard for me.

If you have very complex actions like that, I think an alternative (better?) approach is to have simple synchronous actions instead (you can even just dispatch payloads directly, and drop action creators if you like, reducing boiler-plate), and handle the asynchronous side using redux-saga: https://github.com/yelouafi/redux-saga
Redux Saga makes it very simple to factor out your business logic code into multiple simple generator functions that can be tested in isolation. They can also be tested without the underlying API methods even being called, due to the 'call' function in that library: http://yelouafi.github.io/redux-saga/docs/api/index.html#callfn-args. Due to the use of generators, your test can 'feed' values to the saga using the standard iterator.next method. Finally, they make it much easier for reducers to have their say, since you can check something from store state (e.g. using a selector) to see what to do next in your saga.
If Redux + Redux Saga had existed before I started on my app (about 100,000 JS(X) LOC so far), I would definitely have used them.

Related

Any benefits to use Redux-Saga instead of writing async func in react components?

react version is 16.13.1.
I wondering if there are some benefits to use redux-saga for async methods.
const component = () => {
const asyncFunc = async() => { // <- this part should be moved out to redux-saga?
await callMethod();
}
return (
<div onClick={asyncFunc}>button</div>
)
}
I have no idea that asyncFunc should be called in redux-saga or in react component.
Which is better or more beneficial?
In my opinion, I prefer to call async method in components.
In simpler words redux-saga is beneficial in the case where we need to achieve some async operation during a redux action.
Now what you are doing is handling the side effect in the component so the action you'll dispatch will only update the store.
It is a very simple use case where you handled it in the component, consider a scenario where you need this same functionality from 2 different components.. you will have to copy the logic in 2 different components.
The testing will become difficult.
Now consider the same scenario again but the problem is since you can trigger the API calls from 2 components, let's consider a scenario that the user triggered the API call from both the components simultaneously, it is wastage of resource to handle both the API calls if the first API call is still pending.
for all this scenario redux-saga provide methods like takeLatest, takeEvery etc.
the benefit of using almost each and everything of redux is to organize the code and keep all the states in store, if you use async function in one component and by chance you want to use that async function again for some other component then you have to write the entire code again and again , in case of redux-saga you will write async one time and can call that action anywhere in your whole react project, for now you might be creating 5-10 components but it might be possible that in future you will create 5000 components at that time redux and its middlewares come into play .
Redux-saga is a middleware to act on an action before it reaches the reducer.
Basically, all side effects will be handled in the middleware and gives you more control over the effects.
This way, it has clear separation of concerns that the middleware is going to handle the side effects and not the component. A saga is not dependent on the lifetime of a component.
In a saga, fetch will look something like this:
function* fetchItems(action) {
try {
const result = yield call(axios.post, ...);
yield put ({ type: 'FETCH_SUCCESS', payload: { result } });
} catch (e) {
yield put ({ type: 'FETCH_FAILED', error: { msg: e } });
}
}
yield takeEvery(FETCH_ITEMS, fetchItems);
However for complex systems with background processing, you can implement different patterns that uses fork() and cancel()
function* doSync() {}
function* main() {
while ( yield take(START_SYNC) ) {
const task = yield fork(doSync) // returns a task
yield take(STOP_SYNC)
yield cancel(task) // cancel a task if syncing is stopped
}
}
Thus, all that said, redux-saga's power lies when your system is getting more complex and event-driven.

Is it valid to dispatch multiple times inside redux middleware?

I'm trying to wrap my head around Redux or state management in general for the front-end applications.
As far as I know, there are three basic libraries to create complex actions logic: redux-thunk, redux-saga and redux-observable.
I'm using redux-thunk to create a chain of the async operations, but I found them a little bit inappropriate: action creators should create actions, not functions.
In order to get around this, I've created actions (simple action creators) and operations (thunks) with redux-toolkit, but I still see them a little bit confusing:
reducers.js (they also represent actions):
export function add(state, action) { ... }
export function added(state, action) { ... }
operations.js (they are performing complex actions logic):
export function addItem(payload) {
return async (dispatch) => {
dispatch(actions.add(payload))
const { data } = await Items.create(action.payload);
dispatch(actions.added(payload))
}
}
It looks ok, but I can see myself trying to dispatch add directly in the future.
Using redux-saga seems unnatural thanks to the generator syntax. I don't have to say anything about redux-observable, it's over-complicated for this simple task.
So I tried to use simple custom middleware for this kind of work, but I don't really know if it's a "good practice" or "bad practice". However, it allows to use simple actions to fire an observer that dispatches matching function:
middlewares/observer.js (simplified):
let listeners = {}
export const observer = store => next => action => {
const result = next(action)
if (action.type in listeners) {
listeners[action.type](store.dispatch, action)
}
return result
}
export const createListener = (action, listener) => {
listeners[action.type] = listener
}
Code above allows to write "observers" / "listeners" like below:
createListener(actions.add, async (dispatch, action) => {
const { data } = await Items.create(action.payload)
dispatch(actions.added(data))
})
...which allows to dispatch callback attached to the listened actions. Looks very simple and clean for me.
Is this a bad way to solve this problem?
I'm a Redux maintainer and creator of Redux Toolkit.
Thunks exist to allow you to move complex synchronous and semi-complex async logic outside of components. Components typically use action creators to avoid knowing the details of how to define a given action object. Thunk action creators exist to provide parallel syntax, allowing components to kick off logic without needing to know the details of whether it's a simple action dispatch or something more complex.
That said, thunks do not give you a way to respond to dispatched actions, because the only thing the thunk middleware does is look to see if you've passed a thunk function to dispatch, and if so, call it. Sagas and observables both provide APIs that let you run additional logic in response to dispatched actions. See the Redux FAQ entry on "how do I choose between thunks, sagas, and observables?" for more details.
The middleware you've just shown there is a typical example of a simple "action listener" middleware - really a much simpler version of what sagas and observables let you do. In fact, we're hoping to add a similar middleware to Redux Toolkit, but haven't done so yet.
So, yes, the middleware itself is a valid tool to create, but you haven't provided sufficient information on what specific problem you're trying to solve, so I can't say whether it's an appropriate tool for your problem.

when I map my redux state with component state I am getting an error

when you click advanced sports search button I need to display drawer with my api values.
but right now when I map my redux state with component state I am getting an error.
Actions must be plain objects. Use custom middleware for async actions.
can you tell me how to map my state.
so that in future I can fix all my redux issues by myself.
providing code snippet and sandbox below.
all my map state is done in tab-demo.js
https://codesandbox.io/s/rlpv50q8qo
getSportsPlayerHistory = values => {
this.props.fetchHistorySportsDatafromURL();
};
toggleDrawer = (side, open) => () => {
if (open === true) {
this.getSportsPlayerHistory();
}
this.setState({
[side]: open
});
};
const mapDispatchToProps = dispatch => {
return {
onDeleteAllSPORTS: () => {
// console.log("called");
dispatch(deleteAllPosts());
},
addFavoriteSPORTSs: data => {
dispatch(addFavoriteSPORTSs(data));
},
fetchHistorySportsDatafromURL: () => {
dispatch(fetchHistorySportsDatafromURL());
}
};
};
Actions need to return plain objects, your fetchHistorySportsDatafromURL action returns a function. If you make your history reducer function async then you can make an async function to make your API call there and return the result to state.
API call in reducer
This works, but isn't ideal as you want your reducers to be pure functions, as-in, no side-effects, same input always produces the same output
You can also make the API request in the component's callback handler asynchronously and pass the result to the dispatched action.
API call in component then dispatched in action
This is a good solution and works great for small projects, but couples network business logic into your UI display components, which also isn't as ideal since it reduces code re-usability.
If you still want to keep your API logic separate from your component (which is a good thing), redux-thunk is a way to create asynchronous action creators, which is very similar to the pattern of your original code.
API call in action using redux-thunk
This is the most ideal as it completely de-couples business logic from your UI, meaning you can change back-end requests without touching front-end UI, and other components can now also use the same action. Good DRY principal.
Not really sure what you wanted to do with the new state, but this should get you to a good spot to handle that in your mapStateToProps function.

Is it safe to call sagaMiddleware.run multiple times?

I'm using redux and redux-saga in an application to manage state and asynchronous actions. In order to make my life easier, I wrote a class that acts essentially as a saga manager, with a method that "registers" a saga. This register method forks the new saga and combines it with all other registered sagas using redux-saga/effects/all:
class SagasManager {
public registerSaga = (saga: any) => {
this._sagas.push(fork(saga));
this._combined = all(this._sagas);
}
}
This class is then used by my store to get the _combined saga, supposedly after all sagas are registered:
const store = Redux.createStore(
reducer,
initialState,
compose(Redux.applyMiddleware(sagaMiddleware, otherMiddleware)),
);
sagaMiddleware.run(sagasManager.getSaga());
However, I ran into the problem that depending on circumstances (like import order), this doesn't always work as intended. What was happening was that some of the sagas weren't getting registered before the call to sagaMiddleware.run.
I worked around this by providing a callback on SagasManager:
class SagasManager {
public registerSaga = (saga: any) => {
this._sagas.push(fork(saga));
this._combined = all(this._sagas);
this.onSagaRegister();
}
}
And then the store code can use this as
sagasManager.onSagaRegister = () => sagaMiddleware.run(sagasManager.getSaga());
This seems to work, but I can't find in the docs whether this is safe. I did see that .run returns a Task, which has methods for canceling and the like, but since my problem is only in that awkward time between when the store is constructed and the application is rendered I don't that would be an issue.
Can anyone explain whether this is safe, and if not what a better solution would be?
It may depend on what you mean by "safe". What exactly do you mean by that in this case?
First, here's the source of runSaga itself, and where it gets used by the saga middleware.
Looking inside runSaga, I see:
export function runSaga(options, saga, ...args) {
const iterator = saga(...args)
// skip a bunch of code
const env = {
stdChannel: channel,
dispatch: wrapSagaDispatch(dispatch),
getState,
sagaMonitor,
logError,
onError,
finalizeRunEffect,
}
const task = proc(env, iterator, context, effectId, getMetaInfo(saga), null)
if (sagaMonitor) {
sagaMonitor.effectResolved(effectId, task)
}
return task
}
What I'm getting out of that is that nothing "destructive" will happen when you call runSaga(mySagaFunction). However, if you call runSaga() with the same saga function multiple times, it seems like you'll probably have multiple copies of that saga running, which could result in behavior your app doesn't want.
You may want to try experimenting with this. For example, what happens if you have a counter app, and do this?
function* doIncrement() {
yield take("DO_INCREMENT");
put({type : "INCREMENT"});
}
sagaMiddleware.runSaga(doIncrement);
sagaMiddleware.runSaga(doIncrement);
store.dispatch({type : "DO_INCREMENT"});
console.log(store.getState().counter);
// what's the value?
My guess is that the counter would be 2, because both copies of doIncrement would have responded.
If that sort of behavior is a concern, then you probably want to make sure that prior sagas are canceled.
I actually ran across a recipe for canceling sagas during hot-reloading a while back, and included a version of that in a gist for my own usage. You might want to refer to that for ideas.

Where should I put synchronous side effects linked to actions in redux?

(Note: My question was not clearly written, and I was thinking about some things wrong. The current version of the question is just an attempt to write something that could make the accepted answer useful to as many people as possible.)
I want to have an action that adds an item to a store and registers it with an external dependency.
I could use the thunk middleware and write
export function addItem(item) {
return dispatch => {
dispatch(_addItemWithoutRegisteringIt(item));
externalDependency.register(item);
};
}
But the subscribers would be notified before the item was registered, and they might depend on it being registered.
I could reverse the order and write
export function addItem(item) {
return dispatch => {
externalDependency.register(item);
dispatch(_addItemWithoutRegisteringIt(item));
};
}
But I track the item in the external dependency by a unique id that it is natural to only assign in the reducer.
I could register the item in the reducer, but I am given to understand that it is very bad form to do side effects in a reducer and might lead to problems down the line.
So what is the best approach?
(My conclusion is: there are a number of approaches that would work, but probably the best one for my use case is to store a handle into the external dependency in Redux rather than a handle into Redux in the external dependency.)
If you use Redux Thunk middleware, you can encapsulate it in an action creator:
function addItem(id) {
return { type: 'ADD_ITEM', id };
}
function showNotification(text) {
return { type: 'SHOW_NOTIFICATION', text };
}
export function addItemWithNotification(id) {
return dispatch => {
dispatch(addItem(id));
doSomeSideEffect();
dispatch(showNotification('Item was added.');
};
}
Elaborating, based on the comments to this answer:
Then maybe this is the wrong pattern for my case. I don't want subscribers invoked between dispatch(addItem(id)) and doSomeSideEffect().
In 95% cases you shouldn't worry about whether the subscribers were invoked. Bindings like React Redux won't re-render if the data hasn't changed.
Would putting doSomeSideEffect() in the reducer be an acceptable approach or does it have hidden pitfalls?
No, putting side effects into the reducer is never acceptable. This goes against the central premise of Redux and breaks pretty much any tool in its ecosystem: Redux DevTools, Redux Undo, any record/replay solution, tests, etc. Never do this.
If you really need to perform a side effect together with an action, and you also really care about subscribers only being notified once, just dispatch one action and use [Redux Thunk] to “attach” a side effect to it:
function addItem(id, item) {
return { type: 'ADD_ITEM', id, item };
}
export function addItemWithSomeSideEffect(id) {
return dispatch => {
let item = doSomeSideEffect(); // note: you can use return value
dispatch(addItem(id, item));
};
}
In this case you'd need to handle ADD_ITEM from different reducers. There is no need to dispatch two actions without notifying the subscribers twice.
Here is the one point I still definitely don't understand. Dan suggested that the thunk middleware couldn't defer subscriber notification because that would break a common use case with async requests. I still don't understand this this.
Consider this:
export function doSomethinAsync() {
return dispatch => {
dispatch({ type: 'A' });
dispatch({ type: 'B' });
setTimeout(() => {
dispatch({ type: 'C' });
dispatch({ type: 'D' });
}, 1000);
};
}
When would you want the subscriptions to be notified? Definitely, if we notify the subscribers only when the thunk exits, we won't notify them at all for C and D.
Either way, this is impossible with the current middleware architecture. Middleware isn't meant to prevent subscribers from firing.
However what you described can be accomplished with a store enhancer like redux-batched-subscribe. It is unrelated to Redux Thunk, but it causes any group of actions dispatched synchronously to be debounced. This way you'd get one notification for A and B, and another one notification for C and D. That said writing code relying on this behavior would be fragile in my opinion.
I'm still in the process of learning Redux; however my gut instinct says that this is could be a potential candiate for some custom middleware?

Categories

Resources