Search code examples
jsonarchitecturenestedcqrsflux

How do I handle nested API responses in a Flux application?


I'm porting an existing app to Flux and I'm a bit confused about one topic. Say I have several API endpoints that return two- or three-level nested objects.

For example, GET /articles may return a JSON response of schema

articles: article*

article: {
  author: user,
  likers: user*
  primary_collection: collection?
  collections: collection*
}

collection: {
  curator: user
}

As you see, there are all kinds of users at different levels of nesting:

  • articles[i].author
  • articles[i].likers[i]
  • articles[i].primaryCollection.curator
  • articles[i].collections[i].curator

If I want to update UserStore with fresh data any time articles are fetched, I'd have to write a monstrous method that checks all nested entities on article API response. Moreover, there would be a lot of duplication because there are also other API endpoints with different schemas, and sometimes articles are embedded inside users (e.g. GET /user/published).

Is there a cleaner way for Flux stores to extract nested entities out of all API responses?


Solution

  • An approach suggested by Jing Chen (one of Flux creators and evangelists) was to flatten API responses before they reach the Stores. I wrote a small library that does just that: it normalizes

    [{
      id: 1,
      title: 'Some Article',
      author: {
        id: 1,
        name: 'Dan'
      }
    }, {
      id: 2,
      title: 'Other Article',
      author: {
        id: 1,
        name: 'Dan'
      }
    }]
    

    to

    {
      result: [1, 2],
      entities: {
        articles: {
          1: {
            id: 1,
            title: 'Some Article',
            author: 1
          },
          2: {
            id: 2,
            title: 'Other Article',
            author: 1
          }
        },
        users: {
          1: {
            id: 1,
            name: 'Dan'
          }
        }
      }
    }
    

    (Note there is no duplication and the structure is flat.)

    Normalizr lets you:

    • Nest entities inside other entities, objects and arrays
    • Combine entity schemas to express any kind of API response
    • Automatically merge entities with same IDs (with a warning if they differ)
    • Use a custom ID attribute (e.g. slug)

    To use it, you need to define your entities and nesting rules and use them to transform JSON:

    var normalizr = require('normalizr'),
        normalize = normalizr.normalize,
        Schema = normalizr.Schema,
        arrayOf = normalizr.arrayOf;
    
    // First, define a schema:
    
    var article = new Schema('articles'),
        user = new Schema('users'),
        collection = new Schema('collections');
    
    // Define nesting rules:
    
    article.define({
      author: user,
      collections: arrayOf(collection)
    });
    
    collection.define({
      curator: user
    });
    
    
    // Usage:
    
    // Normalize articles
    var articlesJSON = getArticleArray(),
        normalized = normalize(articlesJSON, arrayOf(article));
    
    // Normalize users
    var usersJSON = getUsersArray(),
        normalized = normalize(usersJSON, arrayOf(user));
    
    // Normalize single article
    var articleJSON = getArticle(),
        normalized = normalize(articleJSON, article);
    

    This allows you to normalize any XHR response before passing it to Flux Dispatcher. The Stores will only need to update themselves from the corresponding dictionary:

    // UserStore
    
    UserStore.dispatchToken = AppDispatcher.register(function (payload) {
      var action = payload.action;
    
      switch (action.type) {
      // you can add any normalized API here since that contains users:
      case ActionTypes.RECEIVE_ARTICLES:
      case ActionTypes.RECEIVE_USERS:
    
        // Users will always be gathered in action.entities.users
        mergeInto(_users, action.entities.users);
        UserStore.emitChange();
        break;
      }
    });
    
    
    // ArticleStore
    
    AppDispatcher.register(function (payload) {
      var action = payload.action;
    
      switch (action.type) {
      // you can add any normalized API here since that contains articles:
      case ActionTypes.RECEIVE_ARTICLES:
    
        // Wait for UserStore to digest users
        AppDispatcher.waitFor([UserStore.dispatchToken]);
    
        // Articles will always be gathered in action.entities.articles
        mergeInto(_articles, action.entities.articles);
        ArticleStore.emitChange();
        break;
      }
    });