How to pass reactive data to children using Flow Router and React Layout

Hey, I have a question.

I have AdminLayout with reactive meteor data and a route:

FlowRouter.route('/test', {
  action: function () {
    ReactLayout.render(AdminLayout, {
      content: <AdminPermissions />
    });
  }
});

So, in the top AdminLayout I have getMeteorData() and render:

getMeteorData() {
  return {
    currentUser: Meteor.user()
  };
},
render() {
  <section>
    {this.props.content}
  </section>
}

Now I need to pass this.data.currentUser as props to its child (AdminPermissions), but I don’t know how. The suggestion in the issue seems to be wrong.

I’ve managed to do a hack. Basically, the finished structure is:

<AppComponent> (does nothing, only has the place to insert parts)
  <AdminLayout> (has reactive data and passes it to children)
    <AdminPermissions> (finally has the props)

Code:

  • App Component (only has areas to insert parts)
AppComponent = React.createClass({
  render() {
    return (
      <section>
        {this.props.layout}
      </section>
    )
  }
});
  • AdminLayout (has reactive meteor data and sends props)
AdminLayout = React.createClass({
  mixins: [ReactMeteorData],
  getMeteorData() {
    return {
      currentUser: Meteor.user(),
    };
  },
  render() {
    // HACK or NOT ?
    let component = React.createElement(this.props.component, {
      user: this.data.currentUser
    });

    return (
      <section>
        {component}
      </section>
    );
  }
});
  • AdminPermissions (has props from the reactive source)
AdminPermissions = React.createClass({
  render() {
    return (
      <section>
        {this.props.user.username}
      </section>
    );
  }
});
  • The route, combining 3 components
FlowRouter.route('/test', {
  action: function () {
    ReactLayout.render(AppComponent, {
      layout: <AdminLayout component={AdminPermissions}/>
    });
  }
});

I do believe that top component should have the user reactive state, not the child ones.

Is this the way to pass reactive data to children? Or am I doing something wrong?

@arunoda, @sashko, maybe you can have a look and tell me, if I am thinking and doing it like you will do? Or maybe suggest, how this should be done. I would really appreciate it.

Thanks.

@tmeasday, now is your chance to explain everything about layouts in react!

1 Like

Okay, I managed to do what I wanted from the beginning.

Structure

<AdminLayout> (has reactive data and passes it to children)
  <AdminPermissions> (has the props from parent)
  • Router
FlowRouter.route('/test', {
  action: function () {
    ReactLayout.render(AdminLayout, {
      content: AdminPermissions
    });
  }
});
  • AdminLayout (here we have the global user prop to send it to children)
AdminLayout = React.createClass({
  mixins: [ReactMeteorData],
  getMeteorData() {
    return {
      currentUser: Meteor.user()
    };
  },
  render() {
    let content = React.createElement(this.props.content, {
      user: this.data.currentUser
    });

    return (
      <section>
        {content}
      </section>
    );
  }
});
  • AdminPermissions (has the user props from parent)
AdminPermissions = React.createClass({
  render() {
    return (
      <section>
        <p>{this.props.user.username}</p>
      </section>
    );
  }
});

p.s. Don’t forget to check if we have the user

Is this the correct way to do that? :coffee:

@sashko maybe we can add to the guide, how this is advised to be done?

3 Likes

Yes. This is a good pattern. Hope this is also valid as well:

AdminLayout = React.createClass({
  mixins: [ReactMeteorData],
  getMeteorData() {
    return {
      currentUser: Meteor.user()
    };
  },
  render() {
    return (
      <section>
        <this.props.content user={this.data.currentUser}/>
      </section>
    );
  }
});
3 Likes

Looks like you solved it. It gets a bit annoying with this approach if you want to have sub-sub layouts and pass things through the same way (do you pass content and subcontent into the top layout? Gets messy pretty quick).

Your original way would have worked via React.cloneElement(this.props.content, {user}), which is a legitimate way to re-prop a instanciated element, the only problem is that propTypes checks happen before you re-prop it, so if you’d added a .isRequired on the user, you’d see warnings in the console.

Yes, the solution works well for me. I only pass content from the router to the top component (in my case AdminLayout), sub-sub content is passed from there (parent to child, sub child, etc).

I also check in the layout, if the user data exists and only then render the child element. If it is loggindIn() - I show the spinner and if there is no user logged in - he gets 404. So, I think I can check in the child element the propTypes as I will always get the user data.

Hey, sorry if I was a bit terse above.

What I meant was that your first solution of passing the instanciated ComponentClass (a React “Element” to follow the terminology) (i.e <AdminPermissions> rather than AdminPermissions) could have worked also if you called React.cloneElement rather than React.createElement (and thus “re-prop”-ing it).

In some ways this approach is better because you can pre-fill a bunch of props before you pass it in (crucially it’s own content prop). But the downside is that it’s at instanciation time that React checks for required props, so if AdminPermissions required that user was set, you’d get a warning. Which is a bummer and seems an oversight on React core’s part. /Rant

(You can read more about all this if you are interested here: https://facebook.github.io/react/blog/2015/03/03/react-v0.13-rc2.html)

1 Like

Oh, yes, I understood. So, you are saying, I can init the React Element from the Router with its own props (smth like <AdminPermissions title={params.title} />) and then clone it and add user props.

I also understand about the user, when it will load, there will be no user, so I will get a warning. But, as I told, I only render the content if the user exists, so I do smth like this:

render() {
  if (!this.data.loggingIn) {
    if (this.data.currentUser) {
      let content = React.createElement(this.props.content, {
        user: this.data.currentUser
      });

      // you suggest (I will also have the props, passed from the router)
      let content = React.cloneElement(this.props.content, {
        user: this.data.currentUser
      });

      return (
        <section className="admin">
            <AdminNavigation \>
            {content}
        </section>
      );
    } else {
      return <NotFound />
    }
  } else {
    return <Loading />;
  }
}

So, in my AdminPermissions I can set the user prop as required as it will be rendered only if the user is logged in.

My last question, what is better, to createElement or cloneElement as for performance (or any other thing, like the required thing), best practices? I can see, that we can pass with cloneElement the route params instead of using FlowRouter.getParam(). What will you advice to choose?

Thanks

Passing data down through props can get a bit tough to manage after a while. Consider using meteorflux:dispatcher and meteorflux:appstate. These packages have been a life-saver for me on one of my projects.

1 Like

I encountered exactly the same problem in my project, and is glad to see the discussion here. I agree with @shcherbin that it’s nice to add a section of this topic to anyone place of Meteor Guide, Flow Router Documentation or Meteor routing guide from Kadira.

I don’t think this.props.content in the < > will work.

I can’t get this example to work. How can you call this.props.content using <>?

The content was passed from the router as a child to the AdminLayout. like mentioned in here

Okay I got it. I didn’t realize you could move the < /> from the component in React Layout and move it to where you call this.props.content.

I thought @arunoda’s solution was more elegant, but I couldn’t get it to work either. I followed the example linked to in @seanh’s response, but didn’t have any luck. Is there special syntax you need in the router to pass a component as an object without using the </> jsx syntax?

Maybe this will give you some ideas?

Thx, but the component2 I want to pass when rendering {content} is actually a method inside component1, so I don’t think this will work the way I want it to.

Make sure you are using </> only once. Initially I was using it twice, once in the router and again in the main layout. Remove it from the router and use it in the main layout.

The problem is I have to pass a parameter from the route to the component (). I tried using object notation {} to pass the parameter, but no luck.