At what nesting level should components read entities from Stores in Flux?
Asked Answered
J

4

90

I'm rewriting my app to use Flux and I have an issue with retrieving data from Stores. I have a lot of components, and they nest a lot. Some of them are large (Article), some are small and simple (UserAvatar, UserLink).

I've been struggling with where in component hierarchy I should read data from Stores.
I tried two extreme approaches, neither of which I quite liked:

All entity components read their own data

Each component that needs some data from Store receives just entity ID and retrieves entity on its own.
For example, Article is passed articleId, UserAvatar and UserLink are passed userId.

This approach has several significant downsides (discussed under code sample).

var Article = React.createClass({
  mixins: [createStoreMixin(ArticleStore)],

  propTypes: {
    articleId: PropTypes.number.isRequired
  },

  getStateFromStores() {
    return {
      article: ArticleStore.get(this.props.articleId);
    }
  },

  render() {
    var article = this.state.article,
        userId = article.userId;

    return (
      <div>
        <UserLink userId={userId}>
          <UserAvatar userId={userId} />
        </UserLink>

        <h1>{article.title}</h1>
        <p>{article.text}</p>

        <p>Read more by <UserLink userId={userId} />.</p>
      </div>
    )
  }
});

var UserAvatar = React.createClass({
  mixins: [createStoreMixin(UserStore)],

  propTypes: {
    userId: PropTypes.number.isRequired
  },

  getStateFromStores() {
    return {
      user: UserStore.get(this.props.userId);
    }
  },

  render() {
    var user = this.state.user;

    return (
      <img src={user.thumbnailUrl} />
    )
  }
});

var UserLink = React.createClass({
  mixins: [createStoreMixin(UserStore)],

  propTypes: {
    userId: PropTypes.number.isRequired
  },

  getStateFromStores() {
    return {
      user: UserStore.get(this.props.userId);
    }
  },

  render() {
    var user = this.state.user;

    return (
      <Link to='user' params={{ userId: this.props.userId }}>
        {this.props.children || user.name}
      </Link>
    )
  }
});

Downsides of this approach:

  • It's frustrating to have 100s components potentially subscribing to Stores;
  • It's hard to keep track of how data is updated and in what order because each component retrieves its data independently;
  • Even though you might already have an entity in state, you are forced to pass its ID to children, who will retrieve it again (or else break the consistency).

All data is read once at the top level and passed down to components

When I was tired of tracking down bugs, I tried to put all data retrieving at the top level. This, however, proved impossible because for some entities I have several levels of nesting.

For example:

  • A Category contains UserAvatars of people who contribute to that category;
  • An Article may have several Categorys.

Therefore if I wanted to retrieve all data from Stores at the level of an Article, I would need to:

  • Retrieve article from ArticleStore;
  • Retrieve all article's categories from CategoryStore;
  • Separately retrieve each category's contributors from UserStore;
  • Somehow pass all that data down to components.

Even more frustratingly, whenever I need a deeply nested entity, I would need to add code to each level of nesting to additionally pass it down.

Summing Up

Both approaches seem flawed. How do I solve this problem most elegantly?

My objectives:

  • Stores shouldn't have an insane number of subscribers. It's stupid for each UserLink to listen to UserStore if parent components already do that.

  • If parent component has retrieved some object from store (e.g. user), I don't want any nested components to have to fetch it again. I should be able to pass it via props.

  • I shouldn't have to fetch all entities (including relationships) at the top level because it would complicate adding or removing relationships. I don't want to introduce new props at all nesting levels each time a nested entity gets a new relationship (e.g. category gets a curator).

Jowett answered 6/9, 2014 at 14:16 Comment(1)
Thanks for posting this. I just posted a very similar question here: groups.google.com/forum/… Everything I've seen has dealt with flat data structures rather than associated data. I'm surprised not to see any best practices on this.Lifton
J
37

The approach at which I arrived is having each components receive its data (not IDs) as a prop. If some nested component needs a related entity, it's up to the parent component to retrieve it.

In our example, Article should have an article prop which is an object (presumably retrieved by ArticleList or ArticlePage).

Because Article also wants to render UserLink and UserAvatar for article's author, it will subscribe to UserStore and keep author: UserStore.get(article.authorId) in its state. It will then render UserLink and UserAvatar with this this.state.author. If they wish to pass it down further, they can. No child components will need to retrieve this user again.

To reiterate:

  • No component ever receives ID as a prop; all components receive their respective objects.
  • If child components needs an entity, it's parent's responsibility to retrieve it and pass as a prop.

This solves my problem quite nicely. Code example rewritten to use this approach:

var Article = React.createClass({
  mixins: [createStoreMixin(UserStore)],

  propTypes: {
    article: PropTypes.object.isRequired
  },

  getStateFromStores() {
    return {
      author: UserStore.get(this.props.article.authorId);
    }
  },

  render() {
    var article = this.props.article,
        author = this.state.author;

    return (
      <div>
        <UserLink user={author}>
          <UserAvatar user={author} />
        </UserLink>

        <h1>{article.title}</h1>
        <p>{article.text}</p>

        <p>Read more by <UserLink user={author} />.</p>
      </div>
    )
  }
});

var UserAvatar = React.createClass({
  propTypes: {
    user: PropTypes.object.isRequired
  },

  render() {
    var user = this.props.user;

    return (
      <img src={user.thumbnailUrl} />
    )
  }
});

var UserLink = React.createClass({
  propTypes: {
    user: PropTypes.object.isRequired
  },

  render() {
    var user = this.props.user;

    return (
      <Link to='user' params={{ userId: this.props.user.id }}>
        {this.props.children || user.name}
      </Link>
    )
  }
});

This keeps innermost components stupid but doesn't force us to complicate the hell out of top level components.

Jowett answered 6/9, 2014 at 14:16 Comment(1)
Just to add a perspective on top of this answer. Conceptually, you can develop a notion of ownership, Given a Data Collection finalise on the top most view who actually owns the the data(hence listening to its store). Few Examples: 1. AuthData should be owned at App Component any change in Auth should be propagated down as props to all childs by App Component. 2. ArticleData should be owned by ArticlePage or Article List as Suggested in answer. now any child of ArticleList can receive AuthData and/or ArticleData from ArticleList regardless of which component actually fetched that data.Buhl
L
37

Most people start out by listening to the relevant stores in a controller-view component near the top of the hierarchy.

Later, when it seems like a lot of irrelevant props are getting passed down through the hierarchy to some deeply nested component, some people will decided it's a good idea to let a deeper component listen for changes in the stores. This offers a better encapsulation of the problem domain that this deeper branch of the component tree is about. There are good arguments to be made for doing this judiciously.

However, I prefer to always listen at the top and simply pass down all the data. I will sometimes even take the entire state of the store and pass it down through the hierarchy as a single object, and I will do this for multiple stores. So I would have a prop for the ArticleStore's state, and another for the UserStore's state, etc. I find that avoiding deeply nested controller-views maintains a singular entry point for the data, and unifies the data flow. Otherwise, I have multiple sources of data, and this can become difficult to debug.

Type checking is more difficult with this strategy, but you can set up a "shape", or type template, for the large-object-as-prop with React's PropTypes. See: https://github.com/facebook/react/blob/master/src/core/ReactPropTypes.js#L76-L91 http://facebook.github.io/react/docs/reusable-components.html#prop-validation

Note that you may want to put the logic of associating data between stores in the stores themselves. So your ArticleStore might waitFor() the UserStore, and include the relevant Users with every Article record it provides through getArticles(). Doing this in your views sounds like pushing logic into the view layer, which is a practice you should avoid whenever possible.

You might also be tempted to use transferPropsTo(), and many people like doing this, but I prefer to keep everything explicit for readability and thus maintainability.

FWIW, my understanding is that David Nolen takes a similar approach with his Om framework (which is somewhat Flux-compatible) with a single entry point of data on the root node -- the equivalent in Flux would be to only have one controller-view listening to all stores. This is made efficient by using shouldComponentUpdate() and immutable data structures that can be compared by reference, with ===. For immutable data structures, checkout David's mori or Facebook's immutable-js. My limited knowledge of Om primarily comes from The Future of JavaScript MVC Frameworks

Littoral answered 7/9, 2014 at 8:45 Comment(4)
Thank you for a detailed answer! I did consider passing down both entire snapshots of stores state. However this might give me a perf problem because every update of UserStore will cause all Articles on screen to be re-rendered, and PureRenderMixin won't be able to tell which articles need to be updated, and which don't. As for your second suggestion, I thought about it too, but I can't see how I could keep article reference equality if article's user is "injected" on each getArticles() call. This would again potentially give me perf problems.Jowett
I see your point, but there are solutions. For one, you could check in shouldComponentUpdate() if an array of user IDs for an article has changed, which is basically just hand-rolling the functionality and behavior you really want in PureRenderMixin in this specific case.Littoral
Right, and anyway I would only need to do it when I'm certain there are perf problems which should not be the case for stores that update several times per minute max. Thank you again!Jowett
Depending on what kind of application you're building, keeping one single entry point is gonna hurt as soon as you start having more "widgets" on-screen that don't directly belong to the same root. If you forcefully try to do it that way, that root node will have way too much responsibility. KISS and SOLID, even though it's client side.Nolita
N
2

My solution is much simpler. Every component that has its own state is allowed to talk and listen to stores. These are very controller-like components. Deeper nested components that don't maintain state but just render stuff aren't allowed. They only receive props for pure rendering, very view-like.

This way everything flows from stateful components into stateless components. Keeping the statefuls count low.

In your case, Article would be stateful and therefore talks to the stores and UserLink etc. would only render so it would receive article.user as prop.

Nolita answered 6/9, 2014 at 16:22 Comment(3)
I suppose this would work if article had user object property, but in my case it only has userId (because the real user is stored in UserStore). Or am I missing your point? Would you share an example?Jowett
Initiate a fetch for the user in the article. Or change your API backend to make the user id "expandable" so you do get the user at once. Either way keep the deeper nested components simple views and reliant on props only.Nolita
I can't afford to initiate the fetch in article because it needs to be rendered together. As for expandable APIs, we used to have them, but they are very problematic to parse from Stores. I even wrote a library to flatten responses for this very reason.Jowett
T
0

The problems described in your 2 philosophies are common to any single page application.

They are discussed briefly in this video: https://www.youtube.com/watch?v=IrgHurBjQbg and Relay ( https://facebook.github.io/relay ) was developed by Facebook to overcome the tradeoff that you describe.

Relay's approach is very data centric. It is an answer to the question "How do I get just the needed data for each components in this view in one query to the server?" And at the same time Relay makes sure that you have little coupling across the code when a component used in multiple views.

If Relay is not an option, "All entity components read their own data" seems a better approach to me for the situation you describe. I think the misconception in Flux is what a store is. The concept of store exist no to be the place where a model or a collection of objects are kept. Stores are temporary places where your application put the data before the view is rendered. The real reason they exist is to solve the problem of dependencies across the data that goes in different stores.

What Flux is not specifying is how a store relate to the concept of models and collection of objects (a la Backbone). In that sense some people are actually making a flux store a place where to put collection of objects of a specific type that is not flush for the whole time the user keeps the browser open but, as I understand flux, that is not what a store is supposed to be.

The solution is to have another layer where you where the entities necessary to render your view (and potentially more) are stored and kept updated. If you this layer that abstract models and collections, it is not a problem if you the subcomponents have to query again to get their own data.

Throttle answered 16/10, 2015 at 22:3 Comment(1)
As far as I understand Dan's approach keeping different types of objects and referencing them through ids allows us to keep cache of those objects and update them only in one place. How this can be achieved if, say, you have several Articles that reference same user, and then user's name is updated? The only way is update all Articles with new user data. This is exactly the same problem you get when choosing between document vs relational db for your backend. What are your thoughts about that? Thank you.Ballinger

© 2022 - 2024 — McMap. All rights reserved.