Skip to content

Instantly share code, notes, and snippets.

@uberllama
Last active August 29, 2015 14:14
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save uberllama/a9e17127cdec1b08a2b4 to your computer and use it in GitHub Desktop.
Conditional loading of components
# posts/show.html.erb
<%= react_component("ShowPost", render(template: 'posts/show.json.jbuilder')) %>
# posts/show.json.jbuilder
json.post do
json.extract!(@post, :id, :user_id, :title, :body)
end
json.comments(@comments) do |comment|
json.extract!(comment, :id, :user_id, :body)
end if @comments
json.users(@users) do |user|
json.extract!(user, :id, :name)
end if @users
# controllers/posts_controller.rb
def show
@post = Post.find(params[:id])
if some condition
@comments = @post.comments.includes(:user)
@users = @comment.flat_map(&:user).uniq
end
end
// components/show_post.js.jsx
var ShowPost = React.createClass({
propTypes: {
post: React.PropTypes.object.isRequired,
comments: React.PropTypes.array,
users: React.PropTypes.array
},
render: function() {
var comments = null;
if (this.props.comments !== undefined) {
comments = (
<div>
<h2>Comments</h2>
<CommentsList comments={this.props.comments} usres={this.props.users} />
</div>
);
}
return (
<div>
<h1>{this.props.post.title}</h1>
<Post post={this.props.post} />
{comments}
</div>
);
}
});
@mattwondra
Copy link

Here's an example of the same thing with asynchronous data loading. This is not a Flux example, it's a proof-of-concept to show what it's like if the parent component fetches its own data.

You'll also note I renamed the parent to ShowPostController. On our team, any component that fetches data, watches for data changes, and keeps it all in state, is called a Controller. Their only job is to compose stateless components and pass data into them as props.

# posts/show.html.erb
# The only data we need still is the ID of the post to show.
<%= react_component("ShowPost", {postId: @postId}) %>


# posts/show/json.jbuilder — same as yours
json.post do
  json.extract!(@post, :id, :user_id, :title, :body)
end
json.comments(@comments) do |comment|
  json.extract!(comment, :id, :user_id, :body)
end if @comments
json.users(@users) do |user|
  json.extract!(user, :id, :name)
end if @users


# controllers/posts_controller.rb
def show
  @postId = params[:id]
end


# components/show_post.js.jsx
var ShowPostController = React.createClass({
  propTypes: {
    postId: React.PropTypes.number.isRequired
  },

  getInitialState: function() {
    return {
      post: undefined,
      comments: undefined,
      users: undefined
    };
  },

  componentDidMount: function() {
    // Here's where the magic happens — remember, this is a non-Flux version,
    // and probably not the greatest for production, but gets the idea across.
    // Choose your own ajax:
    ajax({
      url: '/posts/show/'+this.props.postId+'.json',
      success: function(postData) {
        // Now that we have the data, we'll update the state, forcing a re-render.
        // We don't care whether postData.comments is defined or not, the render
        // function will deal with that.
        this.setState({
          post: postData.post,
          comments: postData.comments,
          users: postData.users
        });
      }.bind(this)
    });
  },

  render: function() {
    // Since the data might not be here yet, we need to define a possible loading state:
    if (!this.state.posts) {
      return <div><em>Loading the post! Be right with you...</em></div>;
    }

    // The rest is the same as your example except this.props.whatever is replaced
    // with this.state.whatever
    var comments = null;
    if (this.state.comments !== undefined) {
      comments = (
        <h2>Comments</h2>
        <CommentsList comments={this.state.comments} usres={this.state.users} />
      );
    }

    return (
      <div>
        <h1>{this.state.post.title}</h1>
        <Post post={this.state.post} />
        {comments}
      </div>
    );
  }
});

@mattwondra
Copy link

Ok, now a Flux version (of the JS only).

Yes, there's a lot more files here. But if you're using these stores over multiple components, the initial overhead pays out completely over time. You can also define helpers to make the coding DRYer (we have a BaseStore that all our stores extend from, and it takes care of all the repetitive events setup).

# components/show_post.js.jsx
var ShowPostController = React.createClass({
  propTypes: {
    postId: React.PropTypes.number.isRequired
  },

  getInitialState: function() {
    // Maybe the stores have some data, maybe not. We don't care actually
    // because we're going to listen to them for changes later on.
    return this.getStateFromStores();
  },

  getStateFromStores: function() {
    return {
      posts: PostStore.get(this.props.postId),
      comments: CommentStore.getByPostId(this.props.postId),
      users: UserStore.getAll()
    }
  },

  setStateFromStore: function() {
    this.setState(this.getStateFromStores());
  }

  componentDidMount: function() {
    // Listen to the stores for changes. Any time one or all of them change, the
    // component will be re-rendered.
    PostStore.addChangeListener(this.setStateFromStores);
    CommentStore.addChangeListener(this.setStateFromStores);
    UserStore.addChangeListener(this.setStateFromStores);

    // Ok, now we can ask an Action Creator to grab data from the API and send it
    // through the dispatcher!
    PostActionCreator.fetchPost(this.props.postId);
  },

  componentWillUnmount: function() {
    // Remember to unbind those event so we avoid zombie component errors
    PostStore.removeChangeListener(this.setStateFromStores);
    CommentStore.removeChangeListener(this.setStateFromStores);
    UserStore.removeChangeListener(this.setStateFromStores);
  },

  render: function() {
    // Since the data might not be here yet, we need to define a possible loading state:
    if (!this.state.posts) {
      return <div><em>Loading the post! Be right with you...</em></div>;
    }

    // The rest is the same as your example except this.props.whatever is replaced
    // with this.state.whatever
    var comments = null;
    if (this.state.comments !== undefined) {
      comments = (
        <h2>Comments</h2>
        <CommentsList comments={this.state.comments} users={this.state.users} />
      );
    }

    return (
      <div>
        <h1>{this.state.post.title}</h1>
        <Post post={this.state.post} />
        {comments}
      </div>
    );
  }
});


# actions/PostActionCreator.js
PostActionCreator = {
  fetchPost: function(postId) {
    // Choose your own ajax:
    ajax({
      url: '/posts/show/'+this.props.postId+'.json',
      success: function(postData) {
        // Cool, we got the data! Now what? Well, we send it through the Dispatcher
        // so that any stores that want can grab the data and use it!

        // This data payload is the actual "action" in Flux terminology.
        var action = {
          type: 'GET_POST_SUCCESS',
          data: postData
        };

        Dispatcher.dispatch(action);
      }
    });
  }
}


# dispatcher: https://github.com/facebook/flux/blob/master/src/Dispatcher.js
# Won't detail this here, but basically it's a hub for all actions to flow through
# the system. The stores below will register listeners to the dispatcher, and every
# single dispatched action will be processed by them. This is the central part of
# the Flux "one-way data flow" — no data should _ever_ enter the system except through 
# the dispatcher.


# stores/PostStore.js
// Keep a PRIVATE stash for the data. Stores should only have public GET methods,
// and no SET methods. The only way to modify data in a store is to dispatch an
// action that the store listens for.
var _posts = {};

// Extend functionality from EventEmitter. This lets the store emit change events,
// like the ones the Component is listening for.
var PostStore = merge(EventEmitter.prototype, {

  // First three functions are all EventEmitter-related
  emitChange: function() {
    this.emit('CHANGE');
  },

  addChangeListener: function(callback) {
    this.on('CHANGE', callback);
  },

  removeChangeListener: function(callback) {
    this.removeListener('CHANGE', callback);
  },

  // Public getter so components can access the data
  get: function(postId) {
    return _posts[postId];
  }
});

// Listen to every action that goes through the Dispatcher. Only do stuff with the
// actions you care about.
Dispatcher.register(function(action) {
  if (action.type === 'GET_POST_SUCCESS') {
    // Cool! We know that this action has some data for us! So let's set it:
    this._posts[action.data.post.id] = action.data.post;

    // And now, any components that rely on us might want to know that something's
    // changed, so they can re-render. Let's run our emitChange event, and anyone
    // who's addChangeListener'd us will have their callback fired:
    PostStore.emitChange();
  }
});


# The other stores are similar, but they define their own get methods. For example,
# a truncated CommentStore might look like this:

var _comments = {};

var CommentStore = merge(EventEmitter.prototype, {
  // event stuff...

  getByPostId: function(postId) {
    var comments = [];

    // Loop through all the private comments object and make an array of all the
    // comments that have the given post_id. This is how we do related data.
    for (id in _comments) {
      if (_comments[id].post_id === postId) {
        comments.push(_comments[id]);
      }
    }
  }
});

Dispatcher.register(function(action) {
  if (action.type === 'GET_POST_SUCCESS') {
    // There _may_ or _may not_ be comments here. Let's check:
    if (this.action.comments) {
      // Yissssssss
      this.action.comments.forEach(function(comment) {
        _comments[comment.id] = comment;
      });
      CommentStore.emitChange();
    }
  }
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment