remix-run/react-router

How to pass props to components when you use <Route components={{}} /> attribute ?

Closed this issue ยท 29 comments

wzup commented

So react-router's component has components attribute.

<Route path="latest" components={{sidebar: Sidebar, content: ContentLayout}} />

Then in an appropriate component I can reference components through props:

{this.props.sidebar}
{this.props.content}

But without react-router I would do this, I can pass my component custom props:

<Sidebar names={ ['foo', 'bar'] } isOpen={true} num={10} />
<ContentLayout type={ contentType } background={'#fff'} title={titleOne} />

My questions is. How can I pass props to my component when I use components attribute of React Router?

There is an example for this: https://github.com/ReactTraining/react-router/blob/master/examples/passing-props-to-children/app.js

The gist is to use React.cloneElement.

hi @wzup I just got this very problem and I noticed that in the child component i have this.props.route which, in your example you could do (I think):

<Route path="latest" components={{sidebar: Sidebar, content: ContentLayout}} something="foo" />

And in, let's say Sidebar you should be able to do:

this.props.route.something

Hope this helps!

@thenikso what version of React-Router? This does not work in react-router-dom 4.0

@themirror178

In react-router v4, we usually do the following to pass in a ProductPage component:

<Route path='/' component={ProductPage} />

When you want to use props, you can prepare a function that returns your component with the desired props. Here I'm preparing to pass in a toggleSidebarOn prop:

    const MyProductPage = (props) => {
      return (
        <ProductPage 
          toggleSidebarOn={this.toggleSidebarOn.bind(this)}
          {...props}
        />
      );
    }

Then you use the render prop of the <Route /> instead of its component prop. Do not use both and do not use the component prop as this leads to undesired remounting and might break your app (e.g. for me CSS transitions for the sidebar animation stopped working properly).

Using the render prop for the MyProductPage we can then pass in our toggleSidebarOn prop to its target route:

<Router>
  <div>
    <Switch>
      <Route exact path="/products" render={MyProductPage} />
      <Route exact path="/perspectives" component={PerspectivePage}/>
      <Route component={NotFound}/>
    </Switch>
  </div>
</Router>

Hope this helps!

In react-router v4, you can do this in a reusable way so you don't have to hand write boilerplate for each component that needs properties. A couple of functions will do the trick (or head all the way to the bottom of this comment for a way to pass properties with no supporting code). renderMergedProps can be reused in any custom component that will return a <Route> but here we just keep it simple and create a PropsRoute that will pass all the properties to the component that was specified on the PropsRoute.

const renderMergedProps = (component, ...rest) => {
  const finalProps = Object.assign({}, ...rest);
  return (
    React.createElement(component, finalProps)
  );
}

const PropsRoute = ({ component, ...rest }) => {
  return (
    <Route {...rest} render={routeProps => {
      return renderMergedProps(component, routeProps, rest);
    }}/>
  );
}

Use the PropsRoute to create any route you want using any component and the component will get the properties. Below, we have four different components all being passed different properties. For example the Books component below will get the booksGetter property, which is different for the two routes having to do with books.

<Router>
    <Switch>
      <PropsRoute path='/login' component={Login} auth={auth} authenticatedRedirect="/" />
      <PropsRoute path='/allbooks' component={Books} booksGetter={getAllBooks} />
      <PropsRoute path='/mybooks' component={Books} booksGetter={getMyBooks} />
      <PropsRoute path='/trades' component={Trades} user={user} />
    </Switch>
</Router>

For something with a little more logic here is how you could use renderMergedProps to create a <PrivateRoute> that either passes properties to the component, or redirects if the user is not logged in.

const PrivateRoute = ({ component, redirectTo, ...rest }) => {
  return (
    <Route {...rest} render={routeProps => {
      return auth.loggedIn() ? (
        renderMergedProps(component, routeProps, rest)
      ) : (
        <Redirect to={{
          pathname: redirectTo,
          state: { from: routeProps.location }
        }}/>
      );
    }}/>
  );
};

And here is how you could use it. Note that <PropsRoute> is not protected, but <PrivateRoute> is.

<Router>
  <Switch>
    <Route exact path="/" component={Home} />
    <PropsRoute path='/allbooks' component={Books} booksGetter={getAllBooks} />
    <PrivateRoute path='/mybooks' component={Books} redirectTo="/" booksGetter={getMyBooks} />
    <PrivateRoute path='/trades' component={Trades} redirectTo="/" user={user} />
  </Switch>
</Router>

Finally, there is a shorthand way you can pass properties now that it's clear what <PropsRoute> is doing and how to implement components with more logic. For me the <PropsRoute> is a little easier on the eye, but some folks might like the style below which requires no other supporting code.

<Route path='/mybooks' render={routeProps => <Books {...routeProps} booksGetter={getMyBooks}/>} />

This works for me, but only if I remove the ...rest prop. As soon as I add ...rest as incoming property for PrivateRoute my webpack bundling fails. Is there any documentation about the ...rest?

@kimasendorf the ...rest is ES6 spread syntax. You may need to add a babel compiler to webpack. I'm a bit of a beginner at webpack at the moment since I'm using create-react-app which takes care of that for me.

I have a babel compiler running and I am using a few components with ...props. I.e.:

const LogInAndSignUp = (props) => (
  <div>
    <FlatButton
      {...props}
      label="Log in"
      containerElement={<Link to="/login"/>}
    />
    <FlatButton
      {...props}
      label="Sign up"
      containerElement={<Link to="/signup"/>}
    />
  </div>
);

Is ...rest basically the same?

Looks like the spread operator is not part of the babel-preset-es2015 and I need to use babel-preset-stage-2 too. Using the ES7 Spread Operator with Webpack.

easiest solution for me:

<Route path="/abc" render={()=><TestWidget num="2" someProp={100}/>}/>

@yonatanmn Yes, this is my initial approach, but I could not get the params :(

@seeliang

how about:

<Route exact path="/abc" render={props => <TestWidget someProp="2" {...props} />} />

and what I do is I actually spread {...props.match.params} because then on TestWidget you can access the URL parameters with just this.props.urlVariableHere.

(note to googlers; the above works on v4)

this works for me:
<Route exact path={"/"} component={() => <Start socket={socket} addUser={addUser}/>}/>

Wow, thank you both @omarjmh and @Macmee,
i will give a go tomorrow. :)

@omarjmh per the docs, that might not be an ideal approach. "When you use component (instead of render, below) the router uses React.createElement to create a new React element from the given component. That means if you provide an inline function, you are creating a new component every render. This results in the existing component unmounting and the new component mounting instead of just updating the existing component. For inline rendering, use the render prop"

@tchaffee Thanks for the update. so @Macmee 's feedback could be better solution in this case?

@seeliang since I wrote the same same thing as @Macmee a little further back in the comments, I'd have to say yes ;-)

Hi All just to add more clarity on what best worked for me after trying various ways as advised by @tchaffee, @yonatanmn and @Macmee

Also just to add more context: I'm using Redux to manage my state and my AppContainer is mapping state to props and connecting my App. From my App props are passed down to other components.

So when I tried this solution (@yonatanmn and @Macmee):
<Route path="/abc" render={props => <TestWidget someProp="2" {...props} />} />
I only got react-router props example:
Object {match: Object, location: Object, history: Object, staticContext: undefined, path: "/somePath"โ€ฆ}

When I tried @tchaffee solution:
const state = { ...props };
<PropsRoute path="/somePath" component={SomeComponent} state={state} />

With this solution I was able to separate react-router props from redux props.
Object {match: Object, location: Object, history: Object, staticContext: undefined, path: "/somePath", state: Object}
This worked better for me, I thought I would share that.

@tchaffee, @yonatanmn and @Macmee thanks again for posting various ways to do this ๐Ÿ‘

I was having trouble trying to get newly updated props (say based on window size changes) getting passed with @tchaffee 's solution. It would only render once. So I combined the basic idea with the one from @yonatanmn. Thanks guys.

<Route path="/" render={routeProps => <Home {...Object.assign({}, routeProps, customProps)} />} />

This version is passing location, from Router, and re-renders whenever anything in customProps changes.

The issue with the render={()=><Component props={..

Is that the component is only rendered once inside the router. So if you go to that route, and then go back with the history, the component is not rendered again, which is no good if you want to update things.

I'm using a format where I have something of the form:

<Router>
  <App>
    <Route path='/' component={Comp1} />
    <Route path='/login' component={Comp2} />
    <Route path='/account' component={Comp3} />
  </App>
</Route>

In my App component, I'm generating a bunch of info that I'd like to pass as props to the child components Comp1, Comp2, Comp3. This was simple in React Router v3, but now it seems that with v4, the Route components are in the way. How do I pass these props from App into the individual components?

@thenikso

Your code is almost the same as:

<something foo={bar} keyword="stuff">

"Yesterday I went to a place, and I did a thing, and after a time, I did another thing, and then I did a thing with a person."

I can not believe that an essential feature requires that much headache with coding it. Why it is not described in the documentation?

just my 2c:
You can use withRouter function of react-router, it plays well with redux also:

In your Component.jsx:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
...
class Component extends Component {}
// or const Component = (props) => ();
...
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Component));

@bora89 The docs are very terse. More real life examples would definitely help. In order to learn the new version I had to read the docs several times, go to stackoverflow for examples, do experiments, and attempt to answer questions myself on stackoverflow. Once you get it, it all makes sense and it feels like an elegant solution that is very flexible and only does what it needs to do. More examples would help that happen much quicker.

This seems like something the library should support out of the box, as it's a fairly common use case.

Locking this one out. Sorry, but there are tons of various examples above and in the documentation and this conversation is just repeating the same points over and over. This seems to be the preferred winner: #4105 (comment)