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

@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.

Maybe such variant could help you:

action() {
  ReactLayout.render(Layout, {
    content: <Component paramName={ params.paramName } />
  });
}

tl;dr
I think the real problem here is that there is a fundamental flaw in the design of ReactLayout.

When I attempt to use React.createElement as per above in the top-level layout component, I get the following error:

Warning: React.createElement: type should not be null, undefined, boolean, or number. It should be a string (for DOM elements) or a ReactClass (for composite components).

While I am able to pass down props from the top-level component using React.cloneElement, it just somehow feels wrong for the entire view basically to be a cloned component.

Additionally, as per the React docs, this method should only be used in “rare situations” so using it for every view would certainly not be rare.

It seems to me that the real issue here is with ReactLayout. Based on @arunoda’s comment in the original issue posting, it seems as if ReactLayout has been designed with the assumption that the top-level component should only be used for layout.

Imo, that comment reflects a fundamental misunderstanding of React - the idea that your top-level component would basically be a “dumb” component, when I would say that the opposite would be true - your top-level component should actually be a “controller” component.

Am I off base here? How can one manage user state globally in an app using ReactLayout with FlowRouter without having to replicate a check for user state in every view? Is it basically assumed that one will use redux/meteorflux or the like?