Search code examples
reactjsreactjs-fluxfluxredux

How to handle one-to-many relationships in Flux stores


I'm just starting to use flux (with redux for now) and am wondering how relationships are supposed to be handled.
For an example we can use Trello that has boards with columns that contains cards.

One approach would be to have one store/reducer for boards and have all the data in it there but that means some very fat stores since they would have to contain all the actions for columns and cards as well.

Another approach i've seen is separating nested resources into for example BoardStore, ColumnStore and CardStore and use their ids as reference.

Here's an example of where I am a bit confused: you could have an action creator called addCard that does a request to the server to create a card with all the data. If you are doing optimistic update, you would have created a card object in one of your store before but you can't know the id it will have until you get back the request.

So in short:

  • Firing addCard
  • addCard does a request, in the meantime you return an action of type ADD_CARD_TEMP
  • you get the request and return an action of type ADD_CARD where the store/reducer changes the id.

Is there a recommended way to deal with this case? Nested store/reducers look a bit silly to me but otherwise you end up with very complex stores so it looks like a compromise really.


Solution

  • Yes, using ids across multiple stores much like a relational database is the way to do it right.

    In your example, let's say you want to optimistically put a new card in a particular column, and that a card can only be in one column (one column to many cards).

    The cards in your CardStore might look like this:

    _cards: {
      'CARD_1': {
        id: 'CARD_1',
        columnID: 'COLUMN_3',
        title: 'Go to sleep',
        text: 'Be healthy and go to sleep on time.',
      },
      'CARD_2': {
        id: 'CARD_2',
        columnID: 'COLUMN_3',
        title: 'Eat green vegetables',
        text: 'They taste better with onions.',
      },
    }
    

    Note that I can refer to a card by the id, and I can also retrieve the id within the object. This allows me to have methods like getCard(id) and also be able to retrieve the id of a particular card within the view layer. Thus I can have a method deleteCard(id) that is called in response to an action, because I know the id in the view.

    Within the card store, you would have getCardsByColumn(columnID), which would be a simple map over the card objects, and this would produce an array of cards that you could use to render the contents of the column.


    Regarding the mechanics of optimistic updates, and how the use of ids affects it:

    You can use a client-side id that is established within the same closure that will handle the XHR response, and clear the client-side id when the response comes back as successful, or instead roll back on error. The closure allows you to hold on to the client-side id until the response comes back.

    Many people will create a WebAPIUtils module that will contain all the methods related to the closure retaining the client-side id and the request/response. The action creator (or the store) can call this WebAPIUtils module to initiate the request.

    So you have three actions:

    1. initiate request
    2. handle success
    3. handle response

    In response to the action that initiates the request, your store receives the client-side id and creates the record.

    In response to success/error, your store again receives the client-side id and either modifies the record to be a confirmed record with a real id, or instead rolls back the record. You would also want to create a good UX around that error, like letting your user try again.

    Example code:

    // Within MyAppActions
    cardAdded: function(columnID, title, text) {
      var clientID = this.createUUID();
      MyDispatcher.dispatch({
        type: MyAppActions.types.CARD_ADDED,
        id: clientID,
        columnID: columnID,
        title: title,
        text: text,
      });
      WebAPIUtils.getRequestFunction(clientID, "http://example.com", {
        columnID: columnID,
        title: title,
        text: text,
      })(); 
    },
    
    // Within WebAPIUtils
    getRequestFunction: function(clientID, uri, data) {
      var xhrOptions = {
        uri: uri,
        data: data,
        success: function(response) {
          MyAppActions.requestSucceeded(clientID, response);
        },
        error: function(error) {
          MyAppActions.requestErrored(clientID, error);
        },
      };
      return function() {
        post(xhrOptions);
      };
    },
    
    // Within CardStore
    switch (action.type) {
    
      case MyAppActions.types.CARD_ADDED:
        this._cards[action.id] = {
          id: action.id,
          title: action.title,
          text: action.text,
          columnID: action.columnID,
        });
        this._emitChange();
        break;
    
      case MyAppActions.types.REQUEST_SUCCEEDED:
        var tempCard = this._cards[action.clientID];
        this._cards[action.id] = {
          id: action.id,
          columnID: tempCard.columnID,
          title: tempCard.title,
          text: tempCard.text,
        });
        delete this._cards[action.clientID];
        break;
    
      case MyAppActions.types.REQUEST_ERRORED:
        // ...
    }
    

    Please don't get too caught up on the details of the names and the specifics of this implementation (there are probably typos or other errors). This is just example code to explain the pattern.