Why are React components not reactive sometimes?


#1

I’ve got two apps that I am working on at the moment and both are using React + Meteor. I’m running into a weird problem where some components are reactive and others aren’t. For instance, the following AdminPostItem component requires a page refresh to show current information:

Here is the parent component:

C.Admin = React.createClass({
  propTypes: {},
  mixins: [
     ReactMeteorData
  ],

  getInitialState() {
    return {}
  },
  getMeteorData() {
    var postsSub = Meteor.subscribe('all_posts');
    return {
      currentUser: Meteor.user(),
      postsLoading: !postsSub.ready(),
      posts: Posts.find({}, {sort: {created_at: -1}})
    };
    return {
      currentUser: Meteor.user()
    };
  },
  renderPosts() {
    return this.data.posts.map((post) => {
      return (
        <C.AdminPostItem key={ post._id } post={ post } />
      )
    });
  },
  addPost() {
    var title = $('[name="title"]').val();

    Meteor.call("posts/insert", title, function(err, res) {
      if (err) {
        console.log(err.reason);
      } else if (res) {
        $('#add-post').modal('toggle');
        FlowRouter.go("PostEdit", {_id: res}, {});
      }
    });
  },
  render() {

    return (
      <div className="admin-container">
        <div className="admin-page-header">
          <h1>Posts</h1>
          <a href="#" className="pull-right" data-toggle="modal" data-target="#add-post">Add Post</a>

          <div className="modal fade" id="add-post" tabIndex="-1" role="dialog" aria-labelledby="myModalLabel">
            <div className="modal-dialog" role="document">
              <div className="modal-content">
                <div className="modal-header">
                  <button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
                  <h4 className="modal-title" id="myModalLabel">Add Post</h4>
                </div>
                <div className="modal-body">
                  <form id="add-post-form">
                    <div className="form-group">
                      <label htmlFor="title">Title</label>
                      <input type="text" className="form-control" name="title"/>
                    </div>
                  </form>
                </div>
                <div className="modal-footer">
                  <button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
                  <button type="button" className="btn btn-primary" onClick={ this.addPost }>Add Post</button>
                </div>
              </div>
            </div>
          </div>
        </div>
        <div className="admin-page-body">
          { this.renderPosts() }
        </div>
      </div>
    )
  }
});

I’m using Bootstrap so sorry for the excessive markup

Here is the child component that won’t update (the parent doesn’t update when I remove posts either):

C.AdminPostItem = React.createClass({
  propTypes: {
    post: React.PropTypes.object.isRequired
  },
  getInitialState() {
    return {}
  },
  shouldComponentUpdate() {
    return true;
  },
  removePost() {
    if (confirm("Are you sure you wan't to remove this post?")) {
      Meteor.call('posts/remove', this.props.post._id, function(err) {
        if (err) {
          console.log(err.reason);
        } else {
          console.log("Successfully removed post");
        }
      });
    }
  },
  togglePublish(e) {
    const self = this;
    let published = $(e.target).html().trim() === "Published";
    let modifier = {
      '$set': {}
    };

    if (published) {
      modifier['$set'].status = "Draft";
    } else {
      modifier['$set'].status = "Published";
    }

    Meteor.call('posts/update', modifier, this.props.post._id, function(err) {
      if (err) {
        console.log(err.reason);
      } else {
        self.forceUpdate()
      }
    });
  },
  render() {
    let { post } = this.props;
    let link = FlowRouter.path("PostPage", { slug: post.slug }, {});
    let edit_link = FlowRouter.path("PostEdit", { _id: post._id }, {});
    return (
      <div className="admin-post-item" key={ post._id }>
        <h4><a href={ link }>{ post.title }</a></h4>
        <span className="post-status" onClick={ this.togglePublish }>{ post.status } </span><span className="time-ago">Last edited <span data-livestamp={ post.updated_at }></span> - </span>

          <span><a href={ edit_link } className="edit-post">Edit </a>
          or
          <a href="#" onClick={ this.removePost } className="remove-post"> Remove</a></span>
      </div>
    )
  }
});

To recap: When I change the underlying data for a post the AdminPostItem component doesn’t change. In addition, when I remove a post the Admin component doesn’t update and remove the post from the list.

I may be messing something up here, but I am new to React and have no idea why this isn’t reactive. Please let me know if you can spot what I’m doing wrong.


#2

I didn’t read all the code yet, but one thing is that your getmeteordata function has two return statements.


#3

I was looking at these 2 returns too.

And subscription in getMeteorData also looks strange, when we have separate place for them.


#4

@sashko Haha! Good catch. I didn’t notice that. I just removed it and I’m still having the non-reactive problem :frowning: Any other ideas?


#5

2nd component does not seems to have state, so there is no reason to re-render.
And hard to comment when we dont see what is left in 1st component reactive part… which return and queries


#6

@shock According to the React package docs a component is supposed to re render when the state or data changes (from getMeteorData) so I don’t think it’s a problem with state. The weird thing is that I have similar setups in other apps and it’s totally reactive… I could store the data in state but I’m not sure that’s best practice.

I’m not sure what you mean by return and queries? I have copied and pasted everything that is in my component.


#7

Try cutting out things until you have the very bare minimum. Often this can help find an unknown bug.

Next, try and watch the data outside of meteor to make sure it’s a React issue. It should fire when you add new documents (also can you see them in the console?

Tracker.autorun(function(computation) {
   var docs = Posts.find({}); // and also try with opts
   console.log('collection changed', docs);
});

Also you don’t have to use getInitialState if you’re not calling setState anywhere (same with the empty propTypes). It looks like your Modal would be a nice spot to abstract away the bootstrap too :wink:

<BootModal 
  rightButton={AddPostBtn}
  onRightButton={this.handleRightBtn} />
   {content html}
</BootModal>

#8

If you put a .fetch() at the end of your find, it should become reactive.


#9

This is the correct answer. Calling find doesn’t register a dependency, but fetch does. I’m going to add a task to print a warning if you return a cursor from getMeteorData.


#10

a warning would be super useful!


#11

@keithnicholas ahhhhh thank you!! Haha. I’ve been super frustrated trying to figure it out. @sashko a warning would be great! I wasn’t aware that fetch was required.


#12

I just published a new version with the warning!