How to separate the UI from the data layer with nested components?


#1

I’m trying to separate my UI components completely from any Meteor package / data loading dependency. Using containers, it’s pretty easy to do this for one level of UI-container, like:

./containers/ItemContainer.jsx

/*
 * The container for the UI for one item
 * It takes care of loading the data
 */

import React, { Component } from 'react';
import { composeWithTracker } from 'react-komposer';

import ItemUI from '../components/ItemUI.jsx';

import { Items } from '../lib/collections.js';

function composer(props, onData) {
  const { itemId } = props;
  if (Meteor.subscribe('items', itemId).ready()) {
    const item = Items.findOne({ _id: itemId });

    onData(null, {...props, item });
  }
};

export default composeWithTracker(composer)(ItemUI);

./components/ItemUI.jsx

/*
 * The UI for one item
 * It only does UI stuff 
 */
import React, { Component, PropTypes } from 'react';

export default class ItemUI extends Component {

  render() {
    const { item } = this.props;

    return <div> { item.name } </div>;
  }
}
ItemUI.propTypes = {
  item: PropTypes.object.isRequired,
};

Now what happens if ItemUI renders an nested container-component? It sounds like ItemUI would import the container that itself depends on some data loading logic, so ItemUI that is supposed to be pure UI loads data stuff. That’s annoying for testing where I’d like to test UI components independently of any data loading thing.

My guess is that things would now look like:

./containers/ItemContainer.jsx
-> same as before

./components/Item.jsx

/*
 * The UI for one item
 * It should only do UI stuff, BUT has data dependency through its children,
 *  and therefore import data dependent things
 */
import React, { Component, PropTypes } from 'react';

import SubItemContainer from '../containers/SubItemContainer.jsx'; <- this is NOT COOL

export default class ItemUI extends Component {

  render() {
    const { item } = this.props;

    return <div>
      { item.name }
      <SubItemContainer subItemId={ item.subItemId } />
    </div>;
  }
}
ItemUI.propTypes = {
  item: PropTypes.object.isRequired,
};

./containers/SubItemContainer.jsx

/*
 * The container for the UI for one sub item
 * It takes care of loading the data
 */

import React, { Component } from 'react';
import { composeWithTracker } from 'react-komposer';

import SubItemUI from '../components/SubItemUI.jsx';

import { SubItems } from '../lib/collections.js';

function composer(props, onData) {
  const { subItemId } = props;
  if (Meteor.subscribe('subItems', subItemId).ready()) {
    const subItem = SubItems.findOne({ _id: subItemId });

    onData(null, {...props, subItem });
  }
};

export default composeWithTracker(composer)(SubItemUI);

#2

I have the same question, have you figured out what’s the best way to do that?


#3

I have figured out a way to do it, but it doesn’t feel good… I’m passing all Meteor related packages in a Context (see that repo and more precisely the context setting here, the boilerplate and the usage here and here) This sample repo demonstrates more than just that so it might be hard to get. Lmk if you have questions.

So I only define/import those Meteor dependencies in the root of my component tree. It’s then much easier to mock them etc. If only Athmosphere was on npm!