Nice to hear from you. There seems to be few people with redux experience and who put deep thought into the architecture.
Which I’d say is necessary, as redux is meant to ensure the longevity and adaptability of applications, but it’s good usage is not necessarily obvious.
We experienced that sagas are significantly harder to test then reducers. By writing tests which only use the reducer and actions to prepare the state and only selectors to test on it, you:
- Don’t require any mocks
- The state will be consistent (if the requirement ‘there should be no inconsistent state after an action is dispatched’ is followed)
- Doesn’t rely on state structure internals
This results in tests that are pure and therefore very fast and will not break until a bug is introduced or the API changed. Which makes them ideal to test complicated business logic with many edge cases.
I heard sometimes the perspective that the actions are the API of the ‘backend’ and the UI calls them. This would mean that the UI is aware of it’s backend, arguably a bad thing.
I prefer to frame it the other way around. The connected components dispatch actions to signal what the user intends or what he did (depending on how clear the intention is). The higher level sub reducers integrate the different domains of the application together. They define which actions should be handled by which subreducers (which in turn are clearly responsible for a specific substate). So the have a weak dependencies on the things happening in the front end. That means they are not reusable, which is fine, as they only define how to integrate different reusable things together to form this concrete application.
They third level reducers would then know how to manipulate state, but they would know nothing about which action was the cause (separation of concern) and are therefore reusable.
Of course, then comes the asynchronous part.
Sagas do definitely a better job at centralising and separating asynchronous concerns. But we are having some trouble with testing, as they have too much abstraction. Specifically, they abstract away selectors and action dispatches, although the are implementations details, not part of the actual API we want to test.
We have a tool to run the saga and only mock the really asynchronous things, being able to test with selectors on a resulting state. But it’s still quite the overhead.
Therefore, I’d keep sagas as simple as possible, only calling selectors to determine which asynchronous call to make and immediately dispatch the response, leaving it to reducers to make sense out of it.
Error handling could look e.g. like this: There is a selector taking the state and the action to determine if the request was successful and can be integrated. This selector is used by two independent reducers, one displaying an error message if necessary, the other actually integrating the data on success.