Closed willdurand closed 5 years ago
I'd like to do a little research on this first. An alternative is redux-loop which may be a better implementation of side effects. It aims to adhere to one of Elm's key principles:
All of the behavior of your application can be traced through one place, and that behavior can be easily broken apart and composed back together.
redux-loop/redux-loop#131 😞
I'd have to try it out but if the Redux developer tool currently does not trigger redux-loop side effects, that might be a good thing! Triggering side effects in the developer tool was the exact problem I mentioned with sagas: you can't step through your application state one action at a time.
Triggering side effects in the developer tool was the exact problem I mentioned with sagas: you can't step through your application state one action at a time.
I am not sure to understand, you can click any action in the list.
Let's say you were using an app with sagas. The developer tool recorded these actions:
SHOW_USER_PAGE
FETCH_USER
-> this triggers the saga fetchUserAndFriends()
which dispatches more actions:
LOAD_USER
FETCH_FRIENDS
LOAD_USER_FRIENDS
Let's also say that FETCH_USER
renders a loading screen and you wanted to use time travel to jump back to that action and use hot reloading to develop the UX. As soon as you time travel to FETCH_USER
, it re-runs the saga, makes the API request again, and dispatches more actions.
The Redux developer tool does account for this problem and will sort of ignore the side effect actions. However, it still fills up the timeline with side effect actions which makes it very hard to reason about state changes.
You may be able to fulfill the use case I described above of working on the loading screen but you would have a very hard time understanding the user flow that caused a bug in, say, the final LOAD_USER_FRIENDS
action.
I'd like to avoid this problem in a side effect library. I don't know if it's fully avoidable, though.
Disclaimer: I haven't used the redux-loop lib.
The thing I already dislike about react-loop is that now reducers
are responsible for two things:
That will make reducers super complex. We found errors in our addons-frontend reducers while keeping them as "pure" as possible, so while I can see why redux-loop exists, I have mixed feelings.
We wrote sagas with more than one API call in addons-frontend, I wonder how we would do that with redux-loop, in an elegant way I mean.
Yeah, I have similar reservations. I'd like to learn more about redux-loop. Trying to write a prototype with multiple API calls would be a good test of it.
As far as your point here:
[reducers are responsible for] initiating API calls (and dealing with errors)
Their docs point out that isn't quite true. They say that the reducer only describes the action for an API call. It does not initiate the API call.
There is a demo project with two implementations (saga and loop): https://github.com/yiransheng/redux-login-examples
I just found an older write-up about Redux side effects which still seems relevant. I share the concerns they used to evaluate each approach. This is where I heard of redux-loop and the good news is that it still appears to be an active project. https://medium.com/magnetis-backstage/redux-side-effects-and-me-89c104a4b149
Re-reading the redux-loop docs, I find it difficult to understand and verbose (reducers were supposed to be crystal clear and that's not the case anymore). Given that our (only?) side effect is to call the API with fetch
, I would reconsider redux-thunk.
About the devtools and your issue with the actions, I'd like to have a decent devtools integration because that's the only way I can debug things. When I dispatch an action and end up with 3 more actions in the devtools, I know I triggered a side effect and I can retrace the state changes for each of the 4 actions. I do prefer having this over not having any idea of what's going on.
I believe redux-thunk solves your specific issue about FETCH_USER
making the API call, because you would not dispatch FETCH_USER
the same way. You would dispatch a thunk and inside this thunk you can do different things:
const fetchUser = ({ _callApi = callApi }) => async (dispatch) => {
// This could be helpful to set `loading: true` in the reducer
dispatch({ type: 'FETCH_USER' });
// API call
const userResponse = await _callApi(`/users/me`);
// Simple error handling
if (userResponse.error) {
// We could deal with errors in each reducer, or dispatch an action to centralize all the errors in a `errors` reducer, but I am not sure if we want that.
dispatch({ type: 'LOAD_USER_HAS_FAILED', error: userResponse.error });
return;
}
// Load the user in the state
dispatch({ type: 'LOAD_USER', user: userResponse });
// This could be helpful to set another loading indicator, or not. It could be omitted TBH.
dispatch({ type: 'FETCH_FRIENDS' });
// Another API call
const friendsResponse = await _callApi(`/friends/${userResponse.id}`);
// Load friends in the state
dispatch({ type: 'LOAD_FRIENDS', user: userResponse, friends: friendsResponse });
};
You get all these actions in the devtools, and jumping to FETCH_USER
won't re-trigger any API call. Jumping to LOAD_USER
either and because it is stored in the devtools, you would reproduce the exact same call (with the same payload).
This is super readable even though it is already complex. We don't always call the API twice in the same saga on addons-frontend. In addition, it is rather very testable. There are various strategies, one popular is to use a mockStore
that records all the dispatched actions, but we could do something simpler:
it('works', async () => {
const _callApi = jest.fn();
const dispatch = jest.fn();
// 1. inject the callApi mock so that we can control the behavior of the `callApi` calls in the thunk
// 2. inject the dispatch spy so that we can expect things
await fetchUser({ _callApi })(dispatch);
expect(_callApi).toHaveBeenCalled(2);
expect(dispatch).toHaveBeenCalled(4);
});
I really think it is simpler and more straightforward, but that's my 2 cents.
ok, I'll take a look at redux-thunk as part of this
We want to use redux-thunk to compose actions. redux-thunk is a redux middleware that allows to dispatch functions instead of plain old actions. Such functions get the
dispatch
function as first argument and then, one can do whatever should be done.