There is an existing code:
const FooRoute: React.SFC<RouteProps> =
({ component: Component, ...rest }) => {
if (!auth.isFoo()) {
return <Redirect to={{ pathname: '/404' }} />;
}
if ('render' in rest) {
return (
<Route {...rest} render={rest.render} />
);
} else {
return (
Component
?
<Route
{...rest}
render={
(props: RouteComponentProps<{}>) => <Component {...props} />}
/>
: null
);
}
};
There is an error here:
Now how does the getComponent function will look like in this case?
Thought about smth like:
const getComponent = (props: RouteComponentProps<{}>) => () => (
<Component {...props} />
)
Then one can simply:
<Route
{...rest}
render={getComponent}
/>
But in this case Component is undefined. Any clues?
EDIT: Important Note - using TypeScript. So have to pass the Component somehow down into getComponent.
EDIT2: the reason I've used double lambda is because it allows handling situations like this:
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => (foo: string) => {
...
}
with
<button onClick={handleClick('bar')} ... // foo === 'bar'
In your case getComponent is a lambda function which returns another lambda function but not component because of (props) => () => .... Modify your getComponent function to receive Component from render function like below.
const getComponent = (Component: Component) => (props: RouteComponentProps<{}>) => (
<Component {...props} />
)
and modify this 'render' function too.
<Route
{...rest}
render={getComponent(Component)}
/>
Note: You are receiving component as component with small c and using it as Component with capital C.
Related
My Router is a simple component containing public and private routes. I have created an AuthRoute referring to the great tutorial from here
So, my Router looks like:
<Router>
<div>
<Navigation />
<Route exact path={ROUTES.LANDING} component={Landing} />
<Route path={ROUTES.SIGN_UP} component={SignUp} />
<Route path={ROUTES.SIGN_UP_SUCCESS} component={SignUpSuccess} />
<AuthenticationRoute path={ROUTES.HOME} component={Home} />
</div>
</Router>
and my AuthenticationRoute looks like this:
export const AuthenticationRoute = ({ component: Component, ...rest }) => {
const [authChecking, setAuthChecking] = useState(true);
const [{ isAuth }, dispatch] = useStateValue();
useEffect(() => {
checkLoggedIn().then(res => {
setAuthChecking(false);
dispatch({
op: 'auth',
type: 'toggleSessionAuth',
toggleSessionAuth: res
});
});
}, [])
if(authChecking)
return null;
if(!isAuth) {
return <Redirect to='/' />;
}
return <Route {...rest} render={(props) => (
<Component {...props} />
)
} />
}
Everything looks fine, however, my console returns such warning:
Warning: Functions are not valid as a React child. This may happen if you return a Component instead of <Component /> from the render. Or maybe you meant to call this function rather than return it.
I have tried different solutions using component/render etc, however, I could not find a solution to this problem and I have no idea what I am doing wrong.
For testing purposes, instead of rendering Component, I tried to render simple <div>test</div> and it worked fine. However, when I am passing a JSX component in props, it returns the warning shown above.
Implementation oh Home Component (Home.js):
export const Home = () => {
const [{ user }, dispatch] = useStateValue();
const { history } = useReactRouter();
const moveTo = path => {
dispatch({
op: 'other',
type: 'setView',
setView: path
});
history.push(path);
}
return (
<div className="pageMenuWrapper">
<h1 className="homeTitle">Hi {() => user ? `, ${user.username}` : ``}.</h1>
<div className="wrapper">
<Tile image={leagueico} alt="text" onClick={() => moveTo(ROUTES.TEST)}/>
<Tile comingSoon />
</div>
</div>
);
}
export default Home;
Could anyone help me solve this little problem?
const Create = () => {
console.log('rerender !!')
const [parcelType, setParcelType] = useState('paper')
console.log('parcelType =', parcelType)
return (
<Container onClick={() => setParcelType('plastic')}>
<BookingList />
<Card title="Business">
<p>Header</p>
</Card>
</Container>
)
}
export default Create
I want to change parcelType state to 'plastic' when click on Container in Create component. and I want to reset parcelType state to 'paper' when route is change ( Create component re-render ). But when component re-render state is not set to paper
For more details: CreateComponent is re-render when route is change in BookingList component
const BookingList = props => {
const { id } = props.match.params
const containerStyle = useTranslateSpring('-100px', '0')
const itemList = items.map((item, idx) => {
const itemStyle = useTranslateSpring('-100px', '0', '0', 200 + 200 * idx)
const url = `/booking/${item.id}/create`
return (
<ItemContainer
onClick={() => props.history.push(url)}
style={itemStyle}
key={item.id}
isactive={id === item.id}
>
{item.id}
</ItemContainer>
)
})
return <Container style={containerStyle}>{itemList}</Container>
}
export default withRouter(BookingList)
Create Component is render in route by routeTemplate
const Routes = () => (
<Router basename={process.env.REACT_APP_BASE_URL}>
<>
<RouteTemplate
exact
path="/booking/:id/create"
component={Booking.create}
title="Booking"
/>
</>
</Router>
)
and RouteTemplate is render Component wrapped by PageTemplate component
const RouteTemplate = props => {
const {
component: Component,
title,
query,
isAuthenticated,
isLanding,
...rest
} = props
return (
<Route
{...rest}
render={matchProps =>
isAuthenticated ? (
<PageTemplate title={title} isLanding={isLanding}>
<Component {...matchProps} query={query} />
</PageTemplate>
) : (
<Redirect
to={{
pathname: '/',
state: { from: props.location },
}}
/>
)
}
/>
)
}
So I assume you want to reset component's state once route is changed.
This should happen wherever you use functional component + hooks or class-based component with explicit this.state. It's how React works under the hood.
You already have <Create> rendered at the page
Once route is changed <Route> tries to render <Create> element
React sees there is already existing <Create> element and tries to update that instead of re-creating(typically update is much more efficient than re-creating). That's why state is not reset - since it should not reset for updates.
There are different way to handle that.
If such a case happen outside react-router's <Route> I'd suggest use key prop to reset state. But for <Route> it would mean replacing more clear/straightforward <Route path="..." component={Create} /> with more verboose <Route path="..." render={({match}) => <Create match={match} key={match.params.id} />}
So instead let's apply useEffect hook to reset state once props.match.params.id is changed:
const Create = ({ match: {params: {id} } }) => {
useEffect(() => {
setParcelType('paper');
}, [id]);
That should be equal to class-based
state = {
typeOfWhatEver: 'paper'
};
componentDidUpdate(prevProps) {
if(prevProps.match.params.id !== this.props.match.params.id) {
this.setState({
typeOfWhatEver: 'paper'
});
}
}
I have a simple HOC which injects a react context as a prop in the wrappedcomponent.
function withTranslate(WrappedComponent) {
//we forward the ref so it can be used by other
return React.forwardRef((props, ref) => (
<TranslatorContext.Consumer>
{context => (<WrappedComponent {...props} translate={context} ref={ref} />)}
</TranslatorContext.Consumer>)
)
}
Now I want a secondary HOC which uses the same context, but changes some predefined props using this context. I succeed with following code:
export function withTranslatedProps(WrappedComponent,propsToBeTransLated) {
//propsToBetranslated is array with all props which will be given via keys
const translateProps=(translate,props)=>{
const ownProps=Object.assign({},props)
propsToBeTransLated.forEach(p=>{
if(ownProps.hasOwnProperty(p)){
ownProps[p]=translate(ownProps[p])
}
})
return ownProps
}
return React.forwardRef((props, ref) => {
console.log("render contextconsumer")
return (
<TranslatorContext.Consumer>
{context => (
<WrappedComponent {...translateProps(context,props)} ref={ref} />
)}
</TranslatorContext.Consumer>)
})
}
But I almost exactly use the same HOC as withTranslate. Is there a better option (without repeating myself) to do this?
edit
I think i solved it:
const _translateProps=(propsToBeTransLated,translate,props)=>{
const ownProps=Object.assign({},props)
propsToBeTransLated.forEach(p=>{
if(ownProps.hasOwnProperty(p)){
ownProps[p]=translate(ownProps[p])
}
})
return ownProps
}
export function withTranslatedProps(WrappedComponent,propsToBeTransLated) {
//propsToBetranslated is array with all props which will be given via keys
let retrieveProps=propsToBeTransLated?_translateProps.bind(null,propsToBeTransLated):(context,props)=>({translate:context,...props})
return React.forwardRef((props, ref) => {
console.log("render contextconsumer")
return (
<TranslatorContext.Consumer>
{context => (
<WrappedComponent {...retrieveProps(context,props)} ref={ref} />
)}
</TranslatorContext.Consumer>)
})
}
Anyone with other possibly better solutions?
You can reuse withTranslate HOC or use the same HOC adding options.
Reusing withTranslate HOC:
/* function that translate the propsToBeTransLated */
const translateProps = (propsToBeTransLated, translate, props) =>
propsToBeTransLated.reduce((translatedProps, prop) => {
if(props.hasOwnProperty(prop))
translatedProps[prop] = translate(props[prop]);
return translatedProps;
}, {});
export function withTranslatedProps(WrappedComponent, propsToBeTransLated = []) {
// HOC inside HOC
const addTranslationsToProps = WrappedComponentWithContext =>
React.forwardRef((props, ref) => (
<WrappedComponentWithContext
{...props}
{...translateProps(propsToBeTransLated, props.translate, props)}
ref={ref}
/>
)
);
// first call withTranslate to add the context
return addTranslationsToProps(withTranslate(WrappedComponent));
}
Adding options to withTranslate HOC
const translateProps = (propsToBeTransLated, translate, props) =>
propsToBeTransLated.reduce((translatedProps, prop) => {
if(props.hasOwnProperty(prop))
translatedProps[prop] = translate(props[prop]);
return translatedProps;
}, {});
export function withTranslate(WrappedComponent, options) {
const { propsToBeTransLated = [] } = options;
return React.forwardRef((props, ref) => (
<TranslatorContext.Consumer>
{context => (
<WrappedComponent
{...props}
{...translateProps(propsToBeTransLated, context, props)}
translate={context}
ref={ref}
/>
)}
</TranslatorContext.Consumer>
));
}
I'm trying to change the state title value to the value I give in the router, but I do not know why it does not work. This code is compiled, but the title is an empty string all the time.
class Header extends React.Component {
state = {
title: '',
};
updateTitle(title) {
this.setState({ title });
}
render() {
const { title } = this.state;
return (
<Typography>
{title}
</Typography>
<Switch>
<Route
exact
path="/"
render={() => (<DashboardPage updateTitle={this.updateTitle} />)}
title="Dashboard"
/>
<Route
path="/payment"
render={() => (<PaymentPage updateTitle={this.updateTitle} />)}
title="Payment"
/>
</Switch>
)};
You have to bind the Component method to pass .this context. So updateTitle = (title)=> {}
You should wrap this.updateTitle in an arrow function like below. Then you can call updateTitle from DashBoardPage.
<Route
path="/dashboard"
render={() => (<DashBoardPage updateTitle={(title) => this.updateTitle(title)} />)}
title="Dashboard"
/>
For example, your DashBoardPage component could look something like below. By calling updateTitle which we have passed as a prop, we are changing the title in the parent component.
DashBoardPage.js
const DashBoardPage = ({ updateTitle }) => {
updateTitle('paymentPage');
return <div>This is the dashboard page</div>;
};
However, i would strongly advise against doing this. A better approach would be to have a Page component. Then DashBoardPage and PaymentPage return a Page component and pass the title as prop.
Try to modify component, the main idea is, I want create login page, try modify App.js but get error
warning.js?6327:36 Warning: Failed prop type: Invalid prop children
supplied to Switch, expected a ReactNode.
My code is:
class App extends Component {
constructor(props) {
super(props)
}
routeWithSubRoutes(route) {
return (
<Route
key={_.uniqueId()}
exact={route.exact || false}
path={route.path}
render={props => (
// Pass the sub-routes down to keep nesting
<route.component {...props} routes={route.routes || null} />
)}
/>
);
}
render () {
return (
<div className={styles.App}>
<Helmet {...config.app} />
<NavBar />
<Switch>
{routes.map(route => this.routeWithSubRoutes.bind(this,route))}
</Switch>
</div>
)
}
}
export default App;
Code that I try to modify
export default () => {
// Use it when sub routes are added to any route it'll work
const login = () => {
}
const routeWithSubRoutes = route => (
<Route
key={_.uniqueId()}
exact={route.exact || false}
path={route.path}
render={props => (
// Pass the sub-routes down to keep nesting
<route.component {...props} routes={route.routes || null} />
)}
/>
);
var isLogin = false;
if(!isLogin) {
return (
<Login />
)
}
if(isLogin) {
return (
<div className={styles.App}>
<Helmet {...config.app} />
<NavBar />
<Switch>
{routes.map(route => routeWithSubRoutes(route))}
</Switch>
</div>
);
}
};
this code is working, but my not, how to fix this?
Function.bind doesn't call the function, it only binds its context. Instead, you should bind it in the constructur:
class App extends Component {
constructor(props) {
super(props)
this.routeWithSubRoutes = this.routeWithSubRoutes.bind(this)
}
/* ... */
render () {
return (
<div className={styles.App}>
<Helmet {...config.app} />
<NavBar />
<Switch>
{routes.map(route => this.routeWithSubRoutes(route))}
</Switch>
</div>
)
}
}