question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Sagas should rather be totally autonomous

See original GitHub issue

Hello,

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:closed
  • Created 8 years ago
  • Comments:14 (8 by maintainers)

github_iconTop GitHub Comments

11reactions
slorbercommented, Dec 24, 2015

@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 😃

Your UI is simply a function on state, i.e. React(state) = view. Replaying an event log to compute that view doesn’t make any sense. You should let your state container (Redux) handle that computation of final state so that React can render it.

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 consider React(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:

  • You can sync 2 Redux stores that are on 2 different browser (for example imagine someone taking remote control of your local redux app for assistance…)
  • You can project that event log in other systems
  • You can send that event log to the backend and compute bigdata statistics based in UI usage
  • so many possibilities…

Absolutely! You may have non-visible state that needs to be managed. Take analytics data for instance. You might collect that into your state to occasionally ship back to your server.

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.

3reactions
slorbercommented, Dec 24, 2015

@youknowriad

@slorber May be It is because I have not the necessary backend knowledge you have to consider the event log as the source of truth for frontend application. I think I need to see an implementation of this to have a precise idea about this.

Just look at this and it will click: http://www.confluent.io/blog/turning-the-database-inside-out-with-apache-samza/

Redux suggest the state of the store is this source of truth and It works quite well for any frontend application.

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

Read more comments on GitHub >

github_iconTop Results From Across the Web

Autonomy | Internet Encyclopedia of Philosophy
Moral autonomy, usually traced back to Kant, is the capacity to deliberate and to give oneself the moral law, rather than merely heeding...
Read more >
Should Your Driverless Car Hit a Pedestrian to Save Your Life?
Surveys show that people generally believe autonomous vehicles should make an emergency decision for the greatest good — except if it might ...
Read more >
On the future of transportation in an era of automated ... - PNAS
Rather, such questions are more and more focused on how such technologies ... Fully autonomous vehicles are designed to drive themselves.
Read more >
Products Liability and Driverless Cars: Issues and Guiding ...
All but the most fully automated vehicles will be controlled, at least some of the time, by human drivers. Untangling fault for accidents...
Read more >
The Ethics of Autonomous Cars - The Atlantic
Our laws are ill-equipped to deal with the rise of these vehicles (sometimes called “automated”, “self-driving”, “driverless”, and “robot” cars— ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found