简体   繁体   中英

Add props to component of wrapped route in react router v4

I have routes that share the same behauvior, layout, etc. I'd like to pass props from layout to those components (Dashboard and Login) inside Route

My routes.js file is the following one

//imports omited    
export default (
    <AppLayout>
        <Route component={Dashboard} path="/" key="/" />
        <Route component={Login} path="/login" key="/login" />
    </AppLayout>
);

The render method of AppLayout.js has this code

const childrenWithExtraProp = React.Children.map(this.props.children, (child) => {
            return React.cloneElement(child, {
                component: React.cloneElement(child.props.component, {
                    functions: {
                        updateMenuTitle: this.updateTitle //function
                    }
                })
            });
        });

This code results on numerous errors:

Warning: Failed prop type: Invalid prop `component` of type `object` supplied to `Route`, expected `function`.
    in Route
    in AppHeader
    in Router (created by BrowserRouter)
    in BrowserRouter (created by App)
    in App


Check the render method of `Route`.
    in Route
    in AppHeader
    in Router (created by BrowserRouter)
    in BrowserRouter (created by App)
    in App


Uncaught Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.

    Check the render method of `Route`.
        at invariant (invariant.js?7313:42)
        at createFiberFromElement (react-dom.development.js?cada:5753)
        at reconcileSingleElement (react-dom.development.js?cada:7531)
        at reconcileChildFibers (react-dom.development.js?cada:7635)
        at reconcileChildrenAtExpirationTime (react-dom.development.js?cada:7756)
        at reconcileChildren (react-dom.development.js?cada:7747)
        at finishClassComponent (react-dom.development.js?cada:7881)
        at updateClassComponent (react-dom.development.js?cada:7850)
        at beginWork (react-dom.development.js?cada:8225)
        at performUnitOfWork (react-dom.development.js?cada:10224)

At the last project I've worked we where using Routes inside Route, but at React-Router v4 this is not allowed.

Edit: before was something like this:

//Array of routes declared before
export default (
    <Router history={browserHistory}>
        <Route path="/" component={General}>
            <IndexRoute component={Index} />
            {routes}
        </Route>
    </Router>
);

I suspect this is the issue:

component: React.cloneElement(child.props.component, {

child.props.component isn't a rendered component (like <Dashbard /> ), it's a component class (like Dashboard ). cloneElement expects a rendered component. And you can't explicitly pass props extra into a component class .

There are a few ways you can achieve what you're doing. Cloning a Route feels "tricky" to me.

Option 1: Higher order component with updateTitle logic

I would personally try making a higher order component (a function that takes a component class and returns a component class) that adds this prop, and exporting your Dashboard/Login components wrapped in it. Slightly more verbose but not as tricky:

HOC file:

const WithExtraProp = (ContentComponent) => {
    return WithPropWrapper extends Component {

        updateMenuTitle() {...}

        render() {
            // Add extra props here
            return <ContentComponent {...this.props} functions={{ updateMenuTitle: this.updateMenuTitle }}/>
        }

    }
}
export default WithExtraProp;

And in Dashboard

class Dashboard extends Component {...}
export default WithExtraProp(Dashboard);

With this method, you could also do (although I like it less)

<AppLayout>
    <Route component={WithExtraProp(Dashboard)} path="/" key="/" />
    <Route component={WithExtraProp(Login)} path="/login" key="/login" />
</AppLayout>

Option 2: Use <Route render={} /> instead of component={} to add props

If you want to keep your current setup, where you implicitly/"magically" add props , I don't see a way to do it without using the Route's render prop instead of component . This way you can render the component and pass in props normally.

You can keep this the same:

<Route component={Dashboard} path="/" key="/" />

And something like this:

const childrenWithExtraProp = React.Children.map(this.props.children, (child) => {
    // Clone the <Route />, remove the `component` prop, add `render` prop
    return React.cloneElement(child, {

        // Remove the `component` prop from the Route, since you can't use
        // `component` and `render` on a Route together. This way component
        // just becomes an API for this withExtraPropClass to use to find
        // the right component
        component: null,

        render = () => {
            const ChildComponent = child.props.component;
            const functions = {
                updateMenuTitle: this.updateTitle //function
            };
            return <ChildComponent functions={functions} />;
        }
        })
    });
});

Option 3: Make AppLayout a higher order component

This is the same as option 1, where in the end you do this:

//imports omited
export default (
    <Route component={Dashboard} path="/" key="/" />
    <Route component={Login} path="/login" key="/login" />
);

And AppLayout is a higher order component that adds the prop.

const AppLayout = (ContentComponent) => {
    return WithPropWrapper extends Component {

        updateMenuTitle() {...}

        render() {
            // Add extra props here
            return (
                <MyLayoutStuff>
                    <ContentComponent {...this.props} functions={{ updateMenuTitle: this.updateMenuTitle }}/>
                </MyLayoutStuff>
            );
        }

    }
}
export default AppLayout;

and export your components wrapped in the layout:

class Dashboard extends Component {...}
export default AppLayout(Dashboard);

Other thoughts

I personally have been using something closest to #3. Specifically, I have a file like dashboard/Dashboard.js , and in that same folder, dashboard/index.js , which exports Dashboard wrapped in a layout. You can see an example of that pattern at this React boilerplate Github folder .

There are other options too. You could make an <AppRoutes children=[{component: Dashboard, path="/"}, {...}] /> component that doesn't have to deal with cloning. If you need to do things with children on top of just <render> ing them, I generally prefer passing them in as an array instead of child components and mapping over them.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM