4

There is a plenty of discussion on how to communicate with external services in Flux.

It is pretty clear that the basic workflow is to fire an HTTP request, which will eventually dispatch successful or failure action based on the response. You can also optionally dispatch "in progress" action before making the request.

But what if request's parameters depend on store's state? Nobody seems to mention it.

So essentially, based on user interaction with the view, an ACTION is dispatched. Store owns logic on how to transition from current state0 to the next state1 given ACTION. Data from state1 is needed to properly form new HTTP request.

For example, user chooses a new filter on the page, and store decides to also reset pagination. This should lead to a new HTTP request with (new filter value, first page), not (new filter value, current page from state0).

View can not make the HTTP call itself right with user's interaction because it then would have to duplicate store's logic to transition to the next state.

View can not make the HTTP call in its store's onChange handler because at this point it is no longer known what was the origin of the state change.

It looks like a viable option to make store fire the HTTP request in the action handler, after it transitioned to the next state. But this will make this action implicitly initiating HTTP call, which disables neat possibility to have a replayable log of dispatched actions for debugging.

Where should HTTP requests be initiated in Flux?

Community
  • 1
  • 1

2 Answers2

1

Let's start at the bottom:

It looks like a viable option to make store fire the HTTP request in the action handler, after it transitioned to the next state. But this will make this action implicitly initiating HTTP call, which disables neat possibility to have a replayable log of dispatched actions for debugging.

This can be mitigated by not initiating HTTP requests if you're in debugging/replay mode. This works great as long as the only thing you do in your HTTP request handlers is fire actions (e.g. SUCCESS and FAILURE actions). You could implement this with a simple global boolean (if (!debug) { httpReq(...) }), but you could also make the pattern a bit more formal.

In Event Sourcing parlance, you use Gateways for such purposes. In normal operation, the Gateway makes your HTTP requests, and in debugging, you turn the Gateway off (so it doesn't make any HTTP requests).

That said, I think the problem can actually be solved by rethinking where your HTTP requests are made.

So essentially, based on user interaction with the view, an ACTION is dispatched. Store owns logic on how to transition from current state0 to the next state1 given ACTION. Data from state1 is needed to properly form new HTTP request.

In the second link in your question (Where should ajax request be made in Flux app?), I recommend doing your writes in action creators but reads in the stores. If you extrapolate that pattern into your use case, you might end up with something like this (pseudocode and long variable names for clarity):

class DataTable extends React.Component {
  render() {
    // Assuming that the store for the data table contains two sets of data:
    // one for the filter selection and one for the pagination.
    // I'll assume they're passed as props here; this also assumes that
    // this component is somehow re-rendered when the store changes.
    var filter = this.props.filter;
    var start = this.props.start;
    var end = this.props.end;

    var data = this.props.dataTableStore.getDataForPageAndFilter(
      start, end, filter
    );

    // the store will either give us the LOADING_TOKEN,
    // which indicates that the data is still loading,
    // or it will give us the loaded data
    if (data === DataTableStore.LOADING_TOKEN) {
      return this.renderLoading();
    } else {
      return this.renderData(data);
    }
  }
}

class DataTableStore {
  constructor() {
    this.cache = {};
    this.filter = null;
    this.start = 0;
    this.end = 10;
  }

  getDataForPageAndFilter(start, end, filter) {
    var url = HttpApiGateway.urlForPageAndFilter(start, end, filter);

    // in a better implementation, the HttpApiGateway
    // might do the caching automatically, rather than
    // making the store keep the cache
    if (!this.cache[url]) {
      this.cache[url] = DataTableStore.LOADING_TOKEN;

      HttpApiGateway.query(url)
      .then((response) => {
        // success
        var payload = {
          url: url,
          data: response.body
        };
        dispatch(DATA_FETCH_SUCCESS, payload);
      }, (error) => {
        // error
        dispatch(DATA_FETCH_FAIL, { ... });
      });
    }

    return this.cache[url];
  }

  handleChangeFilterAction(action) {
    this.filter = action.payload.filter;
    // the store also decides to reset pagination
    this.start = 0;
    this.end = 10;
    this.emit("change");
  }

  handleDataFetchSuccessAction(action) {
    this.cache[action.payload.url] = data;
    this.emit("change");
  }

  handleDataFetchFailAction(action) {
    // ... 
  }
}

DataTableStore.LOADING_TOKEN = "LOADING"; // some unique value; Symbols work well

You can see that the store is responsible for deciding how to update the pagination and the filter variables, but is not responsible for deciding when HTTP requests should be made. Instead, the view simply requests some data, and if the store doesn't have it in the cache, it will then make the HTTP request.

This also allows the view to pass in any additional local state into the getter (in case the HTTP requests also depends on local state).

Community
  • 1
  • 1
Michelle Tilley
  • 157,729
  • 40
  • 374
  • 311
0

I'm not sure to understand all the parts of the question but will try to answer with some useful insights.


Flux is a bit like a not-yet mature version of EventSourcing / CQRS / Domain-Driven-Design for frontend developers

We use something akin to Flux for years on the backend with a different terminology. We can compare Flux ActionCreators to DDD Commands, and Flux Actions to DDD Events.

A command represent the user intent (LOAD_TIMELINE(filters)). It can be accepted or rejected by a command handler that will eventually publish some events. In an UI this does not make much sens to reject commands as you don't want to display buttons that the user should not click...

An event represent something that has happened (always in the past).

The React app state that drives the UI is then somehow a projection of the event log to a json state. Nothing can be displayed on the UI without an event being fired first.


Answering your questions

But what if request's parameters depend on store's state? Nobody seems to mention it.

In DDD, command handlers can actually be stateful. They can use the app state to know how to handle the command appropriately. Somehow this means that your Flux ActionBuilders can be stateful too (maybe they can use some store data while )

So essentially, based on user interaction with the view, an ACTION is dispatched. Store owns logic on how to transition from current state0 to the next state1 given ACTION. Data from state1 is needed to properly form new HTTP request.

In DDD there is a concept called Saga (or Process Manager). To make it simple, it receives the event stream and can produce new commands.

So basically you can express through a Saga your requirement: when there's an event "FILTERS_UPDATED", fire a command "RELOAD_LIST" with the new filters. I'm pretty sure you can implement something similar with any Flux implementation.

Sagas should rather be disabled when you replay the event log, as replaying the event log should not have side effects like triggering new events.


These kinds of semantics are supported in my Atom-React framework, where stores can act as stateful command handlers or sagas.

Sebastien Lorber
  • 89,644
  • 67
  • 288
  • 419