React Router V4, Using render props with high order component - javascript

I'm using react router V4, when i declare a Route i want wrap my component inside a High Order component, if I use HOC in
export default hoc(Component)
Then I put the component in the render prop, it works.
When I do this
`<Route exact path="/projects" render={(props) => (withNavHOC(<ProjectsContainer {...props}/>))} />`
It returns this error:
Uncaught Error: Route.render(): A valid React element (or null) must be returned. You may have returned undefined, an array or some other invalid object.
Why it's happening? My Hoc works fine, it returns a valid react component:
`
const withNavHOC = (WrappedComponent) => {
return class extends React.Component{
render(){
if(this.props.match.params.id){
console.log("Here");
return(
<div>
<ProjectMenu/>
<WrappedComponent {...this.props}/>
</div>)
}
return(
<div>
<Navigation/>
<WrappedComponent {...this.props}/>
</div>
)
}
}
};`

To HOC which is of course a simple function, we must pass "Component", not <Component/>.

Related

Pass custom Props to Private Route - React Typescript

We have a React component XYZ that has certain UI that needs to be hidden/showed based on the route that has mounted the component.
For eg, if the route is /show -> it shall show a table
if the route is /hide-> it shall hide the table.
react-router-dom is being used, but would not like to use state in history.push(...).
Am looking for a solution that can be achieved while defining the routes so that any developer who used this route later don't have to worry about maintaining the state.
PS: We are using Private Route with TS, and overriding the render method is not possible unless any is used.
interface PrivateRouteProps extends RouteProps {
}
const PrivateRoute: FunctionComponent<PrivateRouteProps> = ({
component: Component,
...rest
}) => {
if (!Component) return null;
return (
<Route
{...rest}
render={(props) => true
? <Component {...props}/> // looking for a way to pass custom props here
: <Redirect to={{ pathname: '/', state: { from: props.location } }} />}
/>
)
}
usage :
<PrivateRoute path='/show' component={XYZ} />
Any help on how can I pass props in this PrivateRoute and then pass it on to the Component will be appreciated.TIA
Props
I am assuming that that the extra props are known and can be passed as props to the PrivateRoute component.
We will define the type PrivateRouteProps as a generic which depends on the type of ExtraProps. It will take the additional passed-down props as an object with the prop name extraProps.
Our PrivateRouteProps will accept all of the normal RouteProps except for component, because we want to override that with our own definition. Our version of the component is one which takes the typical RouteComponentProps and also our ExtraProps (as top-level props).
type PrivateRouteProps<ExtraProps> = Omit<RouteProps, 'component'> & {
component: ComponentType<RouteComponentProps & ExtraProps>
extraProps: ExtraProps;
}
Component
In our render function, we can implement the show/hide toggling by checking the value of props.match.path, which is already provided a part of RouteComponentProps.
When we call the Component, we pass down all of the props which were provided to the render and also all of the extraProps that we passed in to PrivateComponent. We are not having to overwrite the definition of render at all, we're just adding our own extra props on top of what's given. If we didn't, we would get errors because we typed the Component such that it expects to receive ExtraProps.
Our PrivateRoute is generic, so instead of typing the component itself as FunctionComponent, we will type the props.
const PrivateRoute = <ExtraProps extends {}>({
component: Component,
extraProps,
...rest
}: PrivateRouteProps<ExtraProps>) => {
if (!Component) return null;
return (
<Route
{...rest}
render={(props) => props.match.path.startsWith('/show')
? <Component {...extraProps} {...props}/>
: <Redirect to={{ pathname: '/', state: { from: props.location } }} />}
/>
)
}
Usage
Now when we declare a PrivateRoute, we must always pass the appropriate extra props.
const RenderNumber = ({n}: {n: number}) => (
<div>{n}</div>
)
<PrivateRoute path="/show/something" component={RenderNumber} extraProps={{n: 5}}/>
// renders the number 5
Typescript Playground Link

Converting stateless React component having arguments to stateful

Inside my React JS project, I am working on the PrivateRoutes.
I have gone through this example of private routing and authenticating using react-router-dom.
https://reacttraining.com/react-router/web/example/auth-workflow
According to this documentation, they have created a PrivateRoute as a stateless component.
But my requirement is to convert it to stateful React component as I want to connect my PrivateRoute component to redux store.
Here is my code.
stateless component
import React from 'react';
import {Route, Redirect} from 'react-router-dom';
import {auth} from './Authentication';
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={props =>
auth.isAuthenticated ? (
<Component {...props} />
) : (
<Component {...props} action="login"/>
)
}
/>
);
export default PrivateRoute;
I converted this component to stateful React component like this.
stateful React component
import React from 'react';
import {Route, Redirect} from 'react-router-dom';
import {auth} from './Authentication';
import {connect} from 'react-redux';
class PrivateRoute extends React.Component {
render({ component: Component, ...rest }) {
return (
<Route
{...rest}
render={props =>
this.props.customer.isAuthenticated ? (
<Component {...props} />
) : (
<Component {...props} action="login"/>
)
}
/>
);
}
}
export default connect(state => state)(PrivateRoute);
Here, I am reading the data from redux store to check whether the user is authenticated or not.
But the way I am converting the stateless component to stateful isn't correct.
Am I passing the arguments render({ component: Component, ...rest }) correctly?
Will connecting the PrivateRoute with redux store create any problem with props as state=>state will map state to props as well as ...rest will have props object?
Not sure what is happening inside the code.
Update
AppRouter.js
import React from 'react';
import {BrowserRouter, Route, Switch} from 'react-router-dom';
import {TransitionGroup, CSSTransition} from 'react-transition-group';
import PrivateRoute from './PrivateRoute';
import HomePage from './../components/HomePage';
import AboutUs from './../components/AboutUs';
import ContactUs from './../components/ContactUs';
import PageNotFound from './../components/PageNotFound';
import RestaurantList from '../components/RestaurantList';
import RestaurantMenu from '../components/RestaurantMenu';
import UserDetails from '../components/UserDetails';
import OrderConfirmation from '../components/OrderConfirmation';
import CustomerAccount from '../components/CustomerAccount';
import Logout from '../components/sections/Logout';
export default () => {
return (
<BrowserRouter>
<Route render={({location}) => (
<TransitionGroup>
<CSSTransition key={location.key} timeout={300} classNames="fade">
<Switch location={location}>
<Route path="/" component={HomePage} exact={true}/>
<Route path="/about" component={AboutUs} />
<Route path="/contact" component={ContactUs} />
<Route path="/restaurants" component={RestaurantList} />
<Route path="/select-menu" component={RestaurantMenu} />
<PrivateRoute path="/user-details" component={UserDetails} />
<PrivateRoute path="/order-confirmation" component={OrderConfirmation} />
<PrivateRoute path="/my-account" component={CustomerAccount} />
<PrivateRoute path="/logout" component={Logout} />
<Route component={PageNotFound} />
</Switch>
</CSSTransition>
</TransitionGroup>
)} />
</BrowserRouter>
);
}
In general, converting a stateless functional component (SFC) to a Component is done like this:
Create the class shell for it.
Copy the SFC's body to the render method. If the SFC was an arrow function, add a return as necessary to render.
Change any references to props in the render method to this.props (or just add const { props } = this; at the top). SFCs receive their props in their arguments, but a component receives them as arguments to its constructor; the default constructor will save them as this.props.
In your case, it's using destructuring on its arguments, so you could do the same with this.props on the right-hand side of the destructuring:
const { component: Component, ...rest } = this.props;
That's it. In your code, you've added parameters to the render function, but it doesn't get called with any arguments, and you've only changed props to this.props a bit haphazardly (including changing auth.isAuthenticated to this.props.customer.isAuthenticated for some reason).
So applying 1-3 above:
// #1 - the shell
class PrivateRoute extends React.Component {
// #2 - `render`, with the body of the SFC inside
render() {
// #3 - destructure `this.props`
const { component: Component, ...rest } = this.props;
// #2 (part 2) - add `return`
return <Route
{...rest}
render={props =>
auth.isAuthenticated ? (
<Component {...props} />
) : (
<Component {...props} action="login"/>
)
}
/>;
}
}
Your stateful component should be:
class PrivateRoute extends React.Component {
render() {
const { component: Component, ...rest } = this.props;
return (
<Route
{...rest}
render={props =>
this.props.customer.isAuthenticated ? (
<Component {...props} />
) : (
<Component {...props} action="login"/>
)
}
/>
);
}
}
Please see that there is some issue in render parameter of Route. Here you have props as function param but still using this.props.customer, don't know the use case hence please fix it as per your application.
Apart from it Component and all the other data is already there in props of the component. It won't be available in parameter of render method in component. Same destructuring as available in stateless component can be written in render method as shown in code above.
Will connecting the PrivateRoute with redux store create any problem with props?
Yes, it would. The way you have connected to the store will make store data available in props of component but external props passed to component will not be available.
For that you have to handle it in mapStateToProps function:
const mapStateToProps = (state, ownProps) => ({
...state,
...ownProps
});
Here mapStateToProps has second parameter which has the external own props passed to component. So you have to return it as well to make it available in component props.
Now connect would be like:
export default connect(mapStateToProps)(PrivateRoute);
I was having two queries.
1) How to convert to Stateful Functional Component?
2) After connecting to the redux store will the props create a problem?
My first query was solved by the answer provided by T.J.Crowder.
For a second query, I tried connecting the redux store to the PrivateRoute and I did get the data I was looking for.
Here is the code which worked for me.
import React from 'react';
import {Route, Redirect} from 'react-router-dom';
import {connect} from 'react-redux';
class PrivateRoute extends React.Component {
render() {
const { component: Component, ...rest } = this.props;
const {customer} = this.props;
return <Route
{...rest}
render={props =>
customer.isAuthenticated ? (
<Component {...props} />
) : (
<Component {...props} action="login"/>
)
}
/>;
}
}
export default connect(state => state)(PrivateRoute);
Using this code I got the data that is coming from the routes, as well as the redux state inside the props.
This is getting data coming from the routes
const { component: Component, ...rest } = this.props;
This is the data coming from the redux store.
const {customer} = this.props;
#T.J.Crowder has already written how to convert stateless component to stateful component in those 3 steps. so i will just write about connecting component to redux store like you did.
I think connected components should always define mapStateToProps and explicitly declare which data they depend on from the state.
because the connected component rerenders if the connected property changes. so it would be a bad idea to connect the whole application state to a component. as it would mean that wheneever anything changes in application state rerender all connected components.
better we define explicitly like the following that we depend on a property called data (or anything you have) from the state. so in this case this component will only rerender if state.data changes it wont rerender if state.xyz changes.
and this way you can take state.data and name it as you wish so it would not conflict with any existing props of the component.
const mapStateToProps = (state, ownProps) => ({
data: state.data
});
export default connect(mapStateToProps)(PrivateRoute);

React Router: access history in rendered Route component

I know there are quite a few questions on this, but I cannot seem to get it to work: I need to access "history" from the child components that are rendered through the Routes. (It receives props from a redux container).
I need to pass down the history object to the components that are rendered in each Route, so that I can this.props.history.push('/route') in the child components. This application was less dynamic before, so each Route was hardcoded with a component={someComponent}; but I found that in doing the Routes dynamically, you need to use render={() => <someComponent />}.
After changing the Routes from component={} to render={() => ...} I lost history in the child components.
My code is something like:
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import { HashRouter as Router, Link, Route, Redirect } from 'react-router-dom';
import { Nav_Item } from '.'
import DynamicTab from '.';
export default class NavBar extends React.Component {
constructor(props) {
super(props);
this.state={}
}
render() {
let tabs = [];
let routes = [];
this.props.tabs.forEach( function(tab, index) {
tabs.push(<Nav_Item key={tab.name} path_name={'/' + tab.name} tab_text={tab.label} />);
routes.push(<Route key={tab.name} path={'/' + tab.name} render={() => <DynamicTab tabName={tab.name} tabSpecs={tab} />} />);
})
return (
<Router>
<div>
<div>
<ol>
{ tabs }
</ol>
</div>
<Redirect to={'/' + this.props.tabs[0].name} />
{ routes }
</div>
</Router>
)
}
}
When you use render you actually get props. In the docs they have an example like this:
<Route {...rest} render={props => (
<FadeIn>
<Component {...props}/>
</FadeIn>
)}/>
So you should be able to access history from those props.
Another solution would be to conditionally render a <Redirect/> component. So maybe you have some internal state that you use like this:
// in your components render function..
if(this.state.shouldRedirect) {
return (<Redirect to={yourURL} />);
}
this.props.router and you have access to whatever you want.
in your case you can just do:
this.props.router.push("/newurl");
There is no need to pass the history as a separate property if this component is rendered by the router.

React component receiveing props as undefined

I build a project for a school’s records system, in which I build the front-end with React. On the main component of the admin page, I wanted to have a react-router which will navigate through the admin dialogs. As I tried to implement this, the following problem occurred: when trying to pass parameters to a class through the react route component, the child component receives no props.
I have the following react component hierarchy:
class Test extends React.Component {
constructor() {
super();
console.log("in class: " + this.props)
}
render() { return <div>test</div>}
}
class AdminPage extends BasicPage {
/* Other class functions here... */
render() {
let pageBody = "";
if(!this.state.isLoading)
pageBody = (
<Router>
<Switch>
<Route path={"/:schoolName/admin"} component={AdminMenu} exact/>
<Route path={"/:schoolName/admin/view/:id"} exact
component={() => <Test par1="abc" />} />
</Switch>
</Router>
);
return (
<Layout title={ this.state.isLoading ?
TITLE_UNTIL_LOADED :
PAGE_TITLE + this.state.schoolPrefs.heb_name}
subtitle={ this.state.subtitle }
notification={ this.state.notification }
isLoading={ this.state.isLoading }
>
{pageBody}
</Layout>
);
}
}
When I go to /Random Name/admin/view/someID, it prints to the console in class: undefined.
I then wanted to see if the problem is in the passing component or the receiving one. I defined the function otherTest(props) as follows:
function otherTest(props) {
console.log("Function props: " + props);
return (<Test {...props} />);
}
And then changed the route component like so:
<Route path={"/:schoolName/admin/view/:id"} exact
component={otherTest} />
When then I went to /Random Name/admin/view/someID, I saw that the function received the props just fine, but the log within <Test … /> still printed undefined.
I also tried adding <Test param1=”123” /> after the {pageBody} variable in the main render function, but it printed in class: undefined as well.
Does someone know where the problem might be?
Thanks.
You must take props parameter from constructor and then pass it to super.
constructor(props){
super(props);
}
Do not use this.props in constructor beacuse constructor fire only at the creating class moment.
Use this code:
constructor(props) {
super(props);
console.log(props);
}

Passing props from container component to nested route component

How can i share state between AppContainer and Home component?
For example, i want put results object in the state of AppContainer for pass it to all other components.
In this app i'm not using Redux.
I've tried to use React.cloneElement in AppContainer but generate an error:
Element type is invalid: expected a string (for built-in components)
or a class/function (for composite components) but got: undefined. You
likely forgot to export your component from the file it's defined in.
index.js
<Router history={browserHistory}>
<AppContainer>
<Route path="/" component={Home} />
<Route path="search" component={Search} />
</AppContainer>
</Router>
AppContainer.js
render() {
return (
<div className="containerApp">
{this.props.children}
</div>
);
}
React.cloneElement is the right way to go. For example:
render() {
const childProps = {
prop1: this.state.prop1
}
return (
<div className="containerApp">
{React.cloneElement(this.props.children, childProps)}
</div>
);
}
If this gives you the error you were previously receiving then the only reason I can think is that AppContainer is being rendered without any children prior to any route match. You could mitigate this either by conditionally rendering in AppContainer:
render() {
const childProps = {
prop1: this.state.prop1
}
const { children } = this.props;
return (
<div className="containerApp">
{children && React.cloneElement(children, childProps)}
</div>
);
}

Categories

Resources