Displaying reactive data in dumb React components


#1

Hi guys, I’m trying to do something relatively simple with React, and I’m sure other people have run into this same problem but I wanted to know if my solution is appropriate.

I have a container component responsible for providing reactive data to it’s ‘dumb component’, (I know I could have achieve the same result using createContainer, but I did it manually so I could understand better what’s happening behind the scenes):

export default ContentViewerContainer = React.createClass({
  displayName: 'MeteorDataContainer',
  mixins: [ReactMeteorData, PureRenderMixin],

  getMeteorData() {
    var getReactiveData = function({ params }) {
      articlesHandler = Meteor.subscribe('articles');

      contents = Articles.find(
        { categories: { $nin: ['hero_article', 'top_article'] } },
        { sort: { createdAt: -1 } }).fetch()

      return {
        contents,
        articlesHandler,
      }
    }

    return getReactiveData(this.props);
  },

  render() {
    return <ContentViewer {...this.props} {...this.data} />;
  }
});

This container will fetch articles from my collection and pass it to this dumb component (I have omitted the irrelevant parts):

export default class ContentViewer extends Component {
  constructor(props) {
    super(props);

    this.state = {
      selectedArticle: { },
    };
  }

  componentDidMount() {
    Tracker.autorun((computation) => {
      if (this.props.articlesHandler.ready()) {
        if (this.props.contents.length > 0)
          this.setState({ selectedArticle: this.props.contents[0] });
      }
    });
  }

  render() {
    return (
      <div className="content-viewer">
        <div className="article-list">
          { this.renderArticleList() }
        </div>
        <div className="article-preview">
          <div>{ this.state.selectedArticle.content }</div>
        </div>
      </div>
    )
  }
}

The problem here is that I need the selectedArticle state variable to be assigned to the first article object when the article collections has been fetched, I’m not aware of other ways of solving this problem, but this situation where I need to display something that has not been fetched from the server yet is constantly occurring in my Meteor+React apps, and often I have no idea of knowing when the data has arrived in the dumb component.

is it OK to use Tracker inside a React component, or are there better ways to know when the data is ready in the dumb component? I see this as the equivalent of calling a callback function when a ajax request has returned


#2

Your ContentViewer component is not dumb – it contains state. It doesn’t appear you need to use state at all in this situation, because state is only for tracking changes to the UI over time, which it doesn’t look like you are doing because you’re never modifying state on an interaction.

If you need a custom variable value derived from your meteor data, put it in your parent component as so:

export default ContentViewerContainer = React.createClass({
  displayName: 'MeteorDataContainer',
  mixins: [ReactMeteorData, PureRenderMixin],

  getMeteorData() {
    var getReactiveData = function({ params }) {
      let contents = [];
      let selectedArticle = {};

      if (Meteor.subscribe('articles').ready()) {
        contents = Articles.find(
          { categories: { $nin: ['hero_article', 'top_article'] } },
          { sort: { createdAt: -1 } }).fetch();
        selectedArticle = contents.length > 0 ? contents[0] : {};
      }

      return {
        contents,
        selectedArticle,
      };
    }

    return getReactiveData(this.props);
  },

  render() {
    return (
      <ContentViewer
        contents={this.data.contents}
        selectedArticle={this.data.selectedArticle}
      />;
  }
});

Then your child component can truly be dumb:

export default class ContentViewer extends Component {
  render() {
    return (
      <div className="content-viewer">
        <div className="article-list">
          { this.renderArticleList() }
        </div>
        <div className="article-preview">
          <div>{ this.props.selectedArticle.content }</div>
        </div>
      </div>
    )
  }
}

#3

Well, he’s not completely dumb, but the reason I have a selectedArticle variable in the state is because the user will be able to select an article from the article list (which have been mostly omitted in the snippet), and I’ll assign the article selected by the user to that variable and the article preview will display it. The real question though, is how to know, from a “dumb” component (I mean component that doesn’t directly fetch the data), when it’s data is ready to be read.

The reason I have a Tracker computation in the “dumb” component is because I’ve chosen to automatically set the selected article to the first article of the collection, so that the article preview will have something to display even when the user hasn’t selected any articles yet. It works, I just don’t know if it’s a good idea to put a Tracker computation inside a “dumb” React component.


#4

Throw state in the parent component also:

export default ContentViewerContainer = React.createClass({
  displayName: 'MeteorDataContainer',
  mixins: [ReactMeteorData, PureRenderMixin],

  getInitialState() {
    return {
      selectedArticleId: ''
    };
  },

  getMeteorData() {
    var getReactiveData = function({ params }) {
      let articles = [];

      if (Meteor.subscribe('articles').ready()) {
        articles = Articles.find(
          { categories: { $nin: ['hero_article', 'top_article'] } },
          { sort: { createdAt: -1 } }).fetch();
        selectedArticle = this.state.selectedArticleId
          ? Articles.findOne({_id: selectedArticleId })
          : (articles.length > 0 ? articles[0] : {});
      }

      return {
        articles,
        selectedArticle,
      };
    }

    return getReactiveData(this.props);
  },

  updateSelectedArticle(selectedArticleId) {
    this.setState({selectedArticleId});
  },

  render() {
    return (
      <ContentViewer
        articles={this.data.articles}
        onArticleClick={this.updateSelectedArticle}
        selectedArticle={this.data.selectedArticle}
      />;
  }
});

Then in your child, call the function to set the state:

export default class ContentViewer extends Component {
  render() {
    return (
      <div className="content-viewer">
        <div className="article-list">
          { this.renderArticleList() }
        </div>
        {/* put this in renderArticleList item... */
        <div onClick={this.props.onArticleClick.bind(this, articleId)}>
          article id to select
        </div>
        <div className="article-preview">
          <div>{ this.props.selectedArticle.content }</div>
        </div>
      </div>
    )
  }
}

Dumb components retain same output every time given same probs. If you are running tracker autorun within your dumb component, it’s not dumb. Think of dumb as a ‘presentational’ component – it’s not doing ANYTHING smart. Just displays data from props. And your container/smart component handles all business logic, but has NOTHING to do with presentation. Just sends data via props.


#5

That’s indeed a very good solution, I’ve never thought of interacting with the Smart Component’s State through functions. However, what’s the problem with keeping some business logic on the presentation component?


#6

@lerik everything is wrong with it. Presentation is for displaying data, and that’s it. Props in > render. If you want business logic in your presentational component, wrap it in a container containing that business logic.


#7

Ok. But what if, for example, I’m writing a Markup Editor using React, and I make changes to an existing document (which has been retrieved by an Smart Component), is it wrong to save the changes to this document in the Presentation Component? Or should I only interact with Mongo Collections through the Smart Component? How would that work?


#8

Also, shouldn’t you bind you updateSelectedArticle function to the Smart Component’s scope when passing it down as a prop to the Presentation Component?