Sagas should rather be totally autonomous
See original GitHub issueHello,
I’ve seen the real world where some sagas need to be stateful to know if the data needs to be fetched or not:
export default function* root(getState) {
const getUser = login => getState().entities.users[login]
const getRepo = fullName => getState().entities.repos[fullName]
const getStarredByUser = login => getState().pagination.starredByUser[login]
const getStargazersByRepo = fullName => getState().pagination.stargazersByRepo[fullName]
yield fork(watchNavigate)
yield fork(watchLoadUserPage, getUser, getStarredByUser)
yield fork(watchLoadRepoPage, getRepo, getStargazersByRepo)
yield fork(watchLoadMoreStarred, getStarredByUser)
yield fork(watchLoadMoreStargazers, getStargazersByRepo)
}
// Fetches data for a User : user data + starred repos
function* watchLoadUserPage(getUser, getStarredByUser) {
while(true) {
const {login, requiredFields = []} = yield take(actions.LOAD_USER_PAGE)
yield fork(loadUser, login, getUser(login), requiredFields)
yield fork(loadStarred, login, getStarredByUser(login))
}
}
// load user unless it is cached
function* loadUser(login, user, requiredFields) {
if (!user || requiredFields.some(key => !user.hasOwnProperty(key))) {
yield call(fetchUser, login)
}
}
// load next page of repos starred by this user unless it is cached
function* loadStarred(login, starredByUser = {}, loadMore) {
if (!starredByUser.pageCount || loadMore)
yield call(
fetchStarred,
login,
starredByUser.nextPageUrl || firstPageStarredUrl(login)
)
}
I think we already discussed that but I think the Saga should be a totally autonomous process that listen for events and perform effects.
The problem here for me is that getState().entities.users[login]
is actually a state that has the purpose of being displayed to the UI, as it is computed by Redux reducers. So basically you are coupling the way a Saga may perform effects to the UI state. Your saga is not really stateful, but it can use state provided by a dependency (the UI state).
I think the Saga should not know anything about the UI state at all. Refactoring the layout of the UI state should not need to perform any modification to the saga logic.
In backend systems, sagas can be distributed across a cluster of machines, and the saga can’t really (or efficiently) query synchronously the state of the app as it may be stored on other machines. That’s why Sagas are stateful and decoupled on the backend.
Maybe we should not force the user to use this decoupling as it introduces more complexity, but at least give the opportunity for the Saga to really be stateful, instead of reusing the UI state provided by getState. A simple possibility would be to register a reducer to the Saga for example.
See for example a saga implemented in Java here: http://www.axonframework.org/docs/2.0/sagas.html
public class OrderManagementSaga extends AbstractAnnotatedSaga {
private boolean paid = false;
private boolean delivered = false;
private transient CommandGateway commandGateway;
@StartSaga
@SagaEventHandler(associationProperty = "orderId")
public void handle(OrderCreatedEvent event) {
// client generated identifiers (1)
ShippingId shipmentId = createShipmentId();
InvoiceId invoiceId = createInvoiceId();
// associate the Saga with these values, before sending the commands (2)
associateWith("shipmentId", shipmentId);
associateWith("invoiceId", invoiceId);
// send the commands
commandGateway.send(new PrepareShippingCommand(...));
commandGateway.send(new CreateInvoiceCommand(...));
}
@SagaEventHandler(associationProperty = "shipmentId")
public void handle(ShippingArrivedEvent event) {
delivered = true;
if (paid) {
end(); (3)
}
}
@SagaEventHandler(associationProperty = "invoiceId")
public void handle(InvoicePaidEvent event) {
paid = true;
if (delivered) {
end(); (4)
}
}
// ...
}
As you can see, the OrderManagementSaga is created after every OrderCreatedEvent (so many OrderManagementSaga can live at the same time in the system, but this probably does not apply to frontend sagas). These sagas are stateful and have the paid
and delivered
attributes.
This is just the Saga code, but you can guess that there’s in the system another item called Shippement
that stores an attribute delivred
.
This may seem surprising but it is not a problem if the global system stores the same data in multiple places. Each place can pick the data it needs from the events. This permits to avoid introducing new dependencies. The only real shared dependency all the components have is the event log.
The current approach of using Redux’ getState() in Sagas for me is a bit similar to using waitFor
of Flux. It works but creates coupling that can be avoided.
Issue Analytics
- State:
- Created 8 years ago
- Comments:14 (8 by maintainers)
Top GitHub Comments
@timdorr it is not because it’s written in the doc in a simple way to make it easy to understand for event-sourcing new-comers that is it an absolute truth 😃
Browsers and backend systems are not so different: they manage state. The main difference is that the frontend receives the user intent synchronously so it generally handles that intent based on an up-to-date state. I’m pretty sure frontend and backend will be more and more similar in the future, and don’t forget than @gaearon has also been influcend by the Turning the database inside out talk which is about backend primarily 😃
Absolutely not. It does make a lot of sense and it permits to implement features like time-travel. You know what, backend guys are doing time-travel for decades 😃 The saga concept itself is from the backend / eventsourcing world.
Instead of thinking
React(state) = view
, you should considerReact(Redux(eventlog)) = view
If Redux is claimed to be the source of truth it is probably to be simpler to understand, but Redux treats itself the event-log as the source of truth. The beauty of this is that you can use this event log for many other usages:
Please tell me any drawback of storing these statistics outside of the Redux tree if they are not displayed in the UI?
If you ship the event log to the server directly instead of computing the analytics on the client, you are still able to implement reducers in the backend to compute these analytics (in the language of your choice btw!). You never loose any data and can replay that event log 1 year later, on another browser or a backend if you want to. (Shipping the event log still has a network cost however…)
If you have an app in production for 1 year, and you want to introduce a new analytics that count the TodoCreated actions for a given user. If you compute the analytics on the frontend, then you will start with a counter value = 0. If you ship the event log to the backend, and want to introduce that statistic, you have 1 year of historical event-log to compute a counter value: you don’t start at 0 but you have your new stat instantatenously!
Redux is just a system to project an event-log (source of truth) into an easy-to-comsume state (projection of source of truth) for view applications like React. Nothing forces you to use a single projection at a time of your event log.
@youknowriad
Just look at this and it will click: http://www.confluent.io/blog/turning-the-database-inside-out-with-apache-samza/
The source of truth for React is the Redux store. You can put the Redux state into React and it computes the same view.
The source of truth for Redux is the event log. You can put the event log into Redux and it will computes the same state.
The source of truth for the event log is the dom events happening on a given UI. You can trigger the dom events on the same UI and it will produce the same event log.
The thing is some source of truth seems to actually be derived from a former source of truth.
For a long time on the backend we considered the database (ie MySQL / MongoDB) as the source of truth (most of us still do actually). While even internally these databases are using event-logs as the source of truth for technical reasons like replication: isn’t that funny?
You have to consider the source of truth according to what you will want to record / replay and how the derived source of truth should behave after code change. The history of things you record should be immutable: you should rather not change the past, but you can eventually change your interpretation of the past: this is hot reloading.
state sourcing
If you consider state as a source of truth, then you can record state and replay them in the same React app. Here’s a video i’ve done some time ago. If you record only state, you don’t have the event log and then if you change a reducer the state history will remain the same: you can only hot-reload React views
event sourcing
If you record events (or actions) of what has happened, then you can replay these events into redux reducers to recompute the whole history of states, and replay this state history into React to show something. If you change a reducer, then you can compute a new history of state: this is how Redux hot reload works. However you can not modify the event log.
command sourcing
If you choose to record the commands (ie the user intent) then you can recompute an event log from the intent log, and then a state log from the event log. The intent is generally translated to events in actionCreators and jsx views where we transform low-level dom-events to Redux actions.
For example imagine a video game in React. When the user press left arrow, an event “WentLeft” is fired. If you hot-reload the JSX or actionCreator so that when left arrow is pressed it actually fires a “Jump”, and you time-travel with Redux, you will see that in your history you still have “WentLeft” because Redux hot reload does not affect the past.
Command sourcing would permit to hot-reload the interpretation layer too and would replace the “WentLeft” by a"Jump" in the event log before computing the state log and before injection states in React. In practice it has not much interest and may be more complicated to do (not sure but maybe ELM is doing this no?)
See also http://stackoverflow.com/questions/9448215/tools-to-support-live-coding-as-in-bret-victors-inventing-on-principle-talk/31388262#31388262