Where should ajax request be made in Flux app?
Asked Answered
M

6

199

I'm creating a react.js application with flux architecture and I am trying figure out where and when a request for data from the server should be made. Is there a any example for this. (Not TODO app!)

Mady answered 29/10, 2014 at 14:4 Comment(0)
U
126

I'm a big proponent of putting async write operations in the action creators and async read operations in the store. The goal is to keep the store state modification code in fully synchronous action handlers; this makes them simple to reason about and simple to unit test. In order to prevent multiple simultaneous requests to the same endpoint (for example, double-reading), I'll move the actual request processing into a separate module that uses promises to prevent the multiple requests; for example:

class MyResourceDAO {
  get(id) {
    if (!this.promises[id]) {
      this.promises[id] = new Promise((resolve, reject) => {
        // ajax handling here...
      });
    } 
    return this.promises[id];
  }
}

While reads in the store involve asynchronous functions, there is an important caveat that the stores don't update themselves in the async handlers, but instead fire an action and only fire an action when the response arrives. Handlers for this action end up doing the actual state modification.

For example, a component might do:

getInitialState() {
  return { data: myStore.getSomeData(this.props.id) };
}

The store would have a method implemented, perhaps, something like this:

class Store {
  getSomeData(id) {
    if (!this.cache[id]) {
      MyResurceDAO.get(id).then(this.updateFromServer);
      this.cache[id] = LOADING_TOKEN;
      // LOADING_TOKEN is a unique value of some kind
      // that the component can use to know that the
      // value is not yet available.
    }

    return this.cache[id];
  }

  updateFromServer(response) {
    fluxDispatcher.dispatch({
      type: "DATA_FROM_SERVER",
      payload: {id: response.id, data: response}
    });
  }

  // this handles the "DATA_FROM_SERVER" action
  handleDataFromServer(action) {
    this.cache[action.payload.id] = action.payload.data;
    this.emit("change"); // or whatever you do to re-render your app
  }
}
Urbanism answered 29/10, 2014 at 16:20 Comment(9)
Have you tried to put promises inside action payloads? I find it more easy to deal with than dispatching multiple actionsAncier
@SebastienLorber The big draw to flux for me is keeping all state updates in a synchronous code path, and explicitly only as a result of action dispatches, so I avoid asynchrony inside the stores.Urbanism
@BrandonTilley - I followed your fluxxor post. very nice and logical! For server-side rendering through fluxxor, we would have to have the Fluxxor.createStore's initialize function load the initial data. How would you suggest going about this? Is there some way to make the flux object (and hence actions) available in the store's initialize (would this even work?)? Or would it be best to just call the api directly?Keijo
@Keijo It's unclear to me still what the "best" solution is. I've been experimenting with this strategy for data loading combined with counting the number of outstanding async requests. Unfortunately flux is injected into stores after construction, so no great way to get actions in the initialize method. You might find some good ideas from Yahoo's isomorophic flux libs; this is something Fluxxor v2 should support better. Feel free to email me if you want to chat about this more.Urbanism
data: result should be data : data, right? there is no result. perhaps better to rename the data param to payload or something like that.Gurney
@BinaryMuse Shouldn't the 'return this.cache[primaryKey]' statement in the code example above be outside of the 'else' branch? In the current situation, the 'LOADING_TOKEN' value will never be returned to the caller of the getSomeData method. Further more, where does the 'primaryKey' value in the 'updateFromServer' method come from? I know this is just a code example, but I was wondering whether I did understand it right or not, because I think I will incorporate this principle in my own react Flux application.Ruffo
@BjörnBoxstart You're correct on the if statement—a typo that has been fixed. I also updated the pseudocode to make it more obvious where the primaryKey comes from (it's in the server response).Urbanism
I found this old thread to be very helpful - in particular, Bill Fisher and Jing Chen's comments. This is very close to what @BinaryMuse is proposing with the minor difference that dispatching happens in the action creator.Aleta
Just to be clear--I want to make sure I understand--your Store requests the data, and upon receiving it, it fires an action, and it also subscribes to that same action, right? (The idea being that other stores could act upon receipt of the same data.)Winshell
A
37

Fluxxor has an example of async communication with an API.

This blog post has talks about it and has been featured on React's blog.


I find this a very important and difficult question that is not clearly answered yet, as frontend software synchronization with the backend is still a pain.

Should API requests be made in JSX components? Stores? Other place?

Performing requests in stores mean that if 2 stores need the same data for a given action, they will issue 2 similar requets (unless you introduce dependencies between stores, which I really don't like)

In my case, I have found this very handy to put Q promises as payload of actions because:

  • My actions do not need to be serializable (I do not keep an event log, I don't need event replay feature of event sourcing)
  • It removes the need to have different actions/events (request fired/request completed/request failed) and have to match them using correlation ids when concurrent requests can be fired.
  • It permits to multiple store to listen to the completion of the same request, without introducing any dependency between the stores (however it may be better to introduce a caching layer?)

Ajax is EVIL

I think Ajax will be less and less used in the near future because it is very hard to reason about. The right way? Considering devices as part of the distributed system I don't know where I first came across this idea (maybe in this inspiring Chris Granger video).

Think about it. Now for scalability we use distributed systems with eventual consistency as storage engines (because we can't beat the CAP theorem and often we want to be available). These systems do not sync through polling each others (except maybe for consensus operations?) but rather use structures like CRDT and event logs to make all the members of the distributed system eventually consistent (members will converge to the same data, given enough time).

Now think about what is a mobile device or a browser. It is just a member of the distributed system that may suffer of network latency and network partitionning. (ie you are using your smartphone on the subway)

If we can build network partition and network speed tolerant databases (I mean we can still perform write operations to an isolated node), we can probably build frontend softwares (mobile or desktop) inspired by these concepts, that work well with offline mode supported out of the box without app features unavailability.

I think we should really inspire ourselves of how databases are working to architecture our frontend applications. One thing to notice is that these apps do not perform POST and PUT and GET ajax requests to send data to each others, but rather use event logs and CRDT to ensure eventual consistency.

So why not do that on the frontend? Notice that the backend is already moving in that direction, with tools like Kafka massively adopted by big players. This is somehow related to Event Sourcing / CQRS / DDD too.

Check these awesome articles from Kafka authors to convince yourself:

Maybe we can start by sending commands to the server, and receiving a stream of server events (through websockets for exemple), instead of firing Ajax requests.

I have never been very comfortable with Ajax requests. As we React developpers tend to be functional programmers. I think it's hard to reason about local data that is supposed to be your "source of truth" of your frontend application, while the real source of truth is actually on the server database, and your "local" source of truth may already be outdated when you receive it, and will never converge to the real source of truth value unless you press some lame Refresh button... Is this engineering?

However it's still a bit hard to design such a thing for some obvious reasons:

  • Your mobile/browser client has limited resources and can not necessarily store all the data locally (thus sometimes requiring polling with an ajax request heavy content)
  • Your client should not see all the data of the distributed system so it requires somehow to filter the events that it receives for security reasons
Ancier answered 29/10, 2014 at 14:49 Comment(7)
Can you provide an example of using Q promises with actions?Gatto
@MattFoxxDuncan not sure it's such a good idea as it makes the "event log" unserializable and makes the store update asynchronously on actions being fired, so it has some drawbacks However If it's ok for your usecase and you understand these drawbacks it's quite handy and reduce boilerplate. With Fluxxor you can probably do something like this.dispatch("LOAD_DATA", {dataPromise: yourPromiseHere});Ancier
Completely disagree about your AJAX argument. In fact it was very annoying to read. Have you read your remarks? Think of stores, games, apps that make serious money - all require API and AJAX server calls.. look at Firebase if you want "serverless" or something of that nature but AJAX is here to say I hope at least no one else agrees with your logicForan
@Foran I'm not saying Ajax will disappear totally in the year (and be sure I'm still building websites on top of ajax requests currently as a CTO of a startup), but I'm saying it'l likely to disappear because it's not a protocol good enough to handle eventual consistency which rather require streaming and not polling, and eventual consistency is what permits to make apps work offline reliably (yes you can hack something with localstorage yourself, but you'll have limited offline capacities, or your app is very simple). The problem is not caching, it's invalidating that cache.Ancier
@Foran The models behind Firebase, Meteor etc is not good enough. Do you know how these systems handle concurrent writes? last-write-win instead of causal consitency/merging strategies? Can you afford overriding work of your collegue in an app when both are working on unreliable connections? Also note that these systems tend to couple a lot the local and server modelisations. Do you know any well known collaborative app that is significantly complex, works perfectly offline, declaring being a satisfied Firebase user? I don'tAncier
@Foran I'm not sure what we disagree on them, because I agree that current web specs are not yet good enough to build the kind of architecture I propose here, and that we still need to rely on Ajax today to make money. However please reconsider my opinions after reading some resource on the subject of log/event-stream based backend systems, distributed systems theory... A good pragmatic introduction is confluent.io/blog/…Ancier
Hi @SebastienLorber I am reading the article and will share my thoughts shortly. So far, it sounds good.Foran
B
20

You can call for data in either the action creators or the stores. The important thing is to not handle the response directly, but to create an action in the error/success callback. Handling the response directly in the store leads to a more brittle design.

Beaux answered 29/10, 2014 at 18:3 Comment(3)
Can you explain this in more details please? Say i need to make initial data loading from the server. In the controller view i start an action INIT, and the Store starts it's async initialization reflecting to this action. Now, i would go with the idea, that when the Store fetched the data, it would simply emit change, but not start an action. So emitting a change after initialization tells the views that they can get the data from the store. Why is there a need not to emit a change upon successful loading, but starting another action?! ThanksPrissy
Fisherwebdev, about stores calling for data, by doing so, don't you break the Flux paradigm, the only 2 proper ways i can think of to call for data is by using: 1. use a bootstrap class using Actions to load data 2. Views, again using Actions to load dataCarpeting
Calling for data is not the same thing as receiving data. @Jim-Y: you should only emit change once the data in the store has actually changed. Yotam: No, calling for data in the store does not break the paradigm. Data should only be received through actions, so that all the stores may be informed by any new data entering the application. So we can call for data in a store, but when the response comes back, we need to create a new action instead of handling it directly. This keeps the application flexible and resilient to new feature development.Beaux
M
2

I have been using Binary Muse's example from the Fluxxor ajax example. Here is my very simple example using the same approach.

I have a simple product store some product actions and the controller-view component which has sub-components that all respond to changes made to the product store. For instance product-slider, product-list and product-search components.

Fake Product Client

Here is the fake client which you could substitute for calling an actual endpoint returning products.

var ProductClient = {

  load: function(success, failure) {
    setTimeout(function() {
      var ITEMS = require('../data/product-data.js');
      success(ITEMS);
    }, 1000);
  }    
};

module.exports = ProductClient;

Product Store

Here is the Product Store, obviously this is a very minimal store.

var Fluxxor = require("fluxxor");

var store = Fluxxor.createStore({

  initialize: function(options) {

    this.productItems = [];

    this.bindActions(
      constants.LOAD_PRODUCTS_SUCCESS, this.onLoadSuccess,
      constants.LOAD_PRODUCTS_FAIL, this.onLoadFail
    );
  },

  onLoadSuccess: function(data) {    
    for(var i = 0; i < data.products.length; i++){
      this.productItems.push(data.products[i]);
    }    
    this.emit("change");
  },

  onLoadFail: function(error) {
    console.log(error);    
    this.emit("change");
  },    

  getState: function() {
    return {
      productItems: this.productItems
    };
  }
});

module.exports = store;

Now the product actions, which make the AJAX request and on success fire the LOAD_PRODUCTS_SUCCESS action returning products to the store.

Product Actions

var ProductClient = require("../fake-clients/product-client");

var actions = {

  loadProducts: function() {

    ProductClient.load(function(products) {
      this.dispatch(constants.LOAD_PRODUCTS_SUCCESS, {products: products});
    }.bind(this), function(error) {
      this.dispatch(constants.LOAD_PRODUCTS_FAIL, {error: error});
    }.bind(this));
  }    

};

module.exports = actions;

So calling this.getFlux().actions.productActions.loadProducts() from any component listening to this store would load the products.

You could imagine having different actions though which would respond to user interactions like addProduct(id) removeProduct(id) etc... following the same pattern.

Hope that example helps a bit, as I found this a little tricky to implement, but certainly helped in keeping my stores 100% synchronous.

Melitamelitopol answered 23/4, 2015 at 0:18 Comment(0)
D
2

I answered a related question here: How to handle nested api calls in flux

Actions are not supposed to be things that cause a change. They are supposed to be like a newspaper that informs the application of a change in the outside world, and then the application responds to that news. The stores cause changes in themselves. Actions just inform them.

Bill Fisher, creator of Flux https://mcmap.net/q/129831/-flux-dispatch-dispatch-cannot-dispatch-in-the-middle-of-a-dispatch

What you basically should be doing is, stating via actions what data you need. If the store gets informed by the action, it should decide if it needs to fetch some data.

The store should be responsible for accumulating/fetching all the needed data. It is important to note though, that after the store requested the data and gets the response, it should trigger an action itself with the fetched data, opposed to the store handling/saving the response directly.

A stores could look like something like this:

class DataStore {
  constructor() {
    this.data = [];

    this.bindListeners({
      handleDataNeeded: Action.DATA_NEEDED,
      handleNewData: Action.NEW_DATA
    });
  }

  handleDataNeeded(id) {
    if(neededDataNotThereYet){
      api.data.fetch(id, (err, res) => {
        //Code
        if(success){
          Action.newData(payLoad);
        }
      }
    }
  }

  handleNewData(data) {
    //code that saves data and emit change
  }
}
Desalinate answered 3/9, 2015 at 16:32 Comment(0)
I
0

Here's my take on this: http://www.thedreaming.org/2015/03/14/react-ajax/

Hope that helps. :)

Isagogics answered 15/3, 2015 at 18:29 Comment(2)
downvote per the guidelines. putting answers on external sites makes this site less useful, and makes for lower quality answers, lowering the usefulness of the site. external urls will probably break in time too. the downvote does not say anything about the usefulness of the article, which by the way is very good :)Gurney
Good post, but adding a short summary of pros / cons of each approach will get you upvotes. On SO, we shouldn't need to click a link to get the gist of your answer.Thadeus

© 2022 - 2024 — McMap. All rights reserved.