I am trying not to use : if(window.location) to get access to my current route.
I am working on a code where the router is already defined, and the component is already written. I am trying to access the router information from the component because I need to apply a classname depending on the route.
Here's how the router looks like:
export const createRoutes = (dispatch, getState) => (
<Route component={AppLayout} >
<Route path="/" component={Home} onEnter={(nextState, replaceState) => {
console.log('on Enter / = ', nextState);
nextState.test = 'aaaa'; //can't seem to pass states here....
}} />
then in my component:
render() {
//I want to access my super router or location . in fact
// I just want to know if currentURl === '/'
//and after reading the react-router doc 5 times - my head hurts
// but I still hope to get an answer here before putting a window.location
}
`
Depending on which version of React Router you're using you have access to different objects available on the context of a component.
MyComponent.contextTypes = {
location: React.PropTypes.object
}
which will enable you to do the following
render() {
if (this.context.location.pathname === '/') {
return <h1>Index</h1>;
} else {
return <h2>Not index</h2>;
}
}
Related
i 'm traying to use Redirect from react-router-dom 4.3.1.
All the logic i apply into mi app.js and i do a get for the token and if exist i have to redirect the user to the homepage..
Also i try to use history, but i dosn 't work.. where is the problem?
app.js
import { Redirect } from 'react-router-dom'
async componentDidMount() {
try{
const urlToken = new URL(window.location)
const authToken = urlToken.searchParams.get('auth_token')
console.log('MATCH ',authToken)
const tokenUser = await getTokenUser(authToken)
console.log('tokenUser', tokenUser.status)
if(tokenUser.status == 200) {
this.setState({ toHome: true })
}
}catch(error){console.log('NOT')}
}
render(){
if(this.state.toHome == true) {
return <Redirect to='/dashboard' />
}
}
The purpose of componentDidMount is not to return a React element. If you want to redirect the user here, you will have to do it programmatically.
One thing that you have to remember is that the Redirect should be part of the returned JSX in your render method.
You have to create a render method in your component. You can return in also conditionally if you want to check something before redirecting. It would look something like:
render () {
return (
<div>
{(condition) && <Redirect />}
</div>)
I'm trying to render container component if path matches.
class MyTopComponent extends Component {
componentDidUpdate() {
const params = getUrlParams(this.props.location);
if (params.id && params.id !== this.props.id) {
this.props.updateId(params.id);
}
}
render() {
const { a, b, c } = getUrlParams(this.props.location);
return this.props.id && this.props.isDataReady ? (
<div>
<Switch>
<Route path={MY_FIRST_PATH} component={MyContainer}/>
</Switch>
</div>
) : null;
}
}
The problem is that after going to path, then back, then to path again, page crashes.
It works fine when I use inline function in component:
<Route path={MY_FIRST_PATH} component={() => <MyContainer/>}/>
Is the second way good in performance?
If MY_FIRST_PATH is a constanct I assume it will resolve to a string i.e. 'MY_FIRST_PATH', but in your path prop you need to have path='/MY_FIRST_PATH'. Very likely you forgot your /
Say I have the following route defined in a Switch:
<Switch>
<Route path='/:pageId' component={Template} />
<Switch>
In the template I pass :pageId to the API and it returns the content for that page. It all works well, however the app crashes if I pass a :pageId value that does no exists (like I have no page with a slug "contact").
How can I make it redirect to a 404 page in this case so as to avoid the app crashing?
Thanks in advance.
Since you're just passing a variable page id to the Route component and not specifically naming each route, you'll want to have the Template component return a 404 page in the case where the server returns nothing.
Switch will take a fall-through component in the case where none of the available paths match the given path, but that only works in case where you're using specific, named paths, ie /users, not with one route linking to variable page names.
A possible approach is to utilize React 16 error boundaries. Then you can simply throw whenever (as long as it is inside a render method IIRC) you know the route is invalid.
class RouteNotFoundError extends Error {
constructor(...args) {
super(...args)
// extending Error is fickle in a transpiler world - use name check instead of instanceof/constructor
this.name = "RouteNotFoundError"
}
}
const RouteNotFoundBoundary = withRouter(
class RouteNotFoundBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { routeNotFound: undefined }
}
componentWillReceiveProps(nextProps) {
if (this.props.location !== nextProps.location) {
this.setState({routeNotFound: false})
}
}
componentDidCatch(e) {
if (e.name === "RouteNotFoundError") {
this.setState({routeNotFound: this.props.location.key})
} else {
throw e
}
}
render() {
if (this.state.routeNotFound === this.props.location.key) {
return this.props.fallback || <div>Not found!</div>
} else {
return this.props.children
}
}
}
)
const Template = props => {
if (!allMyPages[props.match.params.pageId]) {
throw new RouteNotFoundError()
}
return renderMyPage(allMyPages[props.match.params.pageId])
}
const Example = () => (
<RouteNotFoundBoundary fallback={<div>Oh no!</div>}>
<Switch>
<Route path='/:pageId' component={Template}/>
</Switch>
</RouteNotFoundBoundary>
)
Not sure if it's a good idea or not, but it might simplify some cases when the code path where you know if the route is valid or not is not the best place to render a 404 page.
I am using React Router 4 for routing and Apollo Client for data fetching & caching. I need to implement a PrivateRoute and redirection solution based on the following criteria:
The pages a user is permitted to see are based on their user status, which can be fetched from the server, or read from the cache. The user status is essentially a set of flags we use to understand where the user is in our funnel. Example flags: isLoggedIn, isOnboarded, isWaitlisted etc.
No page should even begin to render if the user's status does not permit them to be on that page. For example, if you aren't isWaitlisted, you are not supposed to see the waitlist page. When users accidentally find themselves on these pages, they should be redirected to a page that is suitable for their status.
The redirection should also be dynamic. For example, say you try to view your user profile before you are isLoggedIn. Then we need to redirect you to the login page. However, if you are isLoggedIn but not isOnboarded, we still don't want you to see your profile. So we want to redirect you to the onboarding page.
All of this needs to happen on the route level. The pages themselves should be kept unaware of these permissions & redirections.
In conclusion, we need a library that given the user status data, can
compute whether a user can be on a certain page
compute where they need to be redirected to dynamically
do these before rendering any page
do these on the route level
I'm already working on a general-use library, but it has its shortcomings right now. I'm seeking opinions on how one should approach this problem, and whether there are established patterns to achieve this goal.
Here is my current approach. This is not working because the data the getRedirectPath needs is in the OnboardingPage component.
Also, I can't wrap the PrivateRoute with the HOC that could inject the props required to compute the redirect path because that would not let me use it as a child of the Switch React Router component as it stops being a Route.
<PrivateRoute
exact
path="/onboarding"
isRender={(props) => {
return props.userStatus.isLoggedIn && props.userStatus.isWaitlistApproved;
}}
getRedirectPath={(props) => {
if (!props.userStatus.isLoggedIn) return '/login';
if (!props.userStatus.isWaitlistApproved) return '/waitlist';
}}
component={OnboardingPage}
/>
General Approach
I would create an HOC to handle this logic for all of your pages.
// privateRoute is a function...
const privateRoute = ({
// ...that takes optional boolean parameters...
requireLoggedIn = false,
requireOnboarded = false,
requireWaitlisted = false
// ...and returns a function that takes a component...
} = {}) => WrappedComponent => {
class Private extends Component {
componentDidMount() {
// redirect logic
}
render() {
if (
(requireLoggedIn && /* user isn't logged in */) ||
(requireOnboarded && /* user isn't onboarded */) ||
(requireWaitlisted && /* user isn't waitlisted */)
) {
return null
}
return (
<WrappedComponent {...this.props} />
)
}
}
Private.displayName = `Private(${
WrappedComponent.displayName ||
WrappedComponent.name ||
'Component'
})`
hoistNonReactStatics(Private, WrappedComponent)
// ...and returns a new component wrapping the parameter component
return Private
}
export default privateRoute
Then you only need to change the way you export your routes:
export default privateRoute({ requireLoggedIn: true })(MyRoute);
and you can use that route the same way you do today in react-router:
<Route path="/" component={MyPrivateRoute} />
Redirect Logic
How you set this part up depends on a couple factors:
How you determine whether a user is logged in, onboarded, waitlisted, etc.
Which component you want to be responsible for where to redirect to.
Handling user status
Since you're using Apollo, you'll probably just want to use graphql to grab that data in your HOC:
return graphql(gql`
query ...
`)(Private)
Then you can modify the Private component to grab those props:
class Private extends Component {
componentDidMount() {
const {
userStatus: {
isLoggedIn,
isOnboarded,
isWaitlisted
}
} = this.props
if (requireLoggedIn && !isLoggedIn) {
// redirect somewhere
} else if (requireOnboarded && !isOnboarded) {
// redirect somewhere else
} else if (requireWaitlisted && !isWaitlisted) {
// redirect to yet another location
}
}
render() {
const {
userStatus: {
isLoggedIn,
isOnboarded,
isWaitlisted
},
...passThroughProps
} = this.props
if (
(requireLoggedIn && !isLoggedIn) ||
(requireOnboarded && !isOnboarded) ||
(requireWaitlisted && !isWaitlisted)
) {
return null
}
return (
<WrappedComponent {...passThroughProps} />
)
}
}
Where to redirect
There are a few different places you can handle this.
Easy way: routes are static
If a user is not logged in, you always want to route to /login?return=${currentRoute}.
In this case, you can just hard code those routes in your componentDidMount. Done.
The component is responsible
If you want your MyRoute component to determine the path, you can just add some extra parameters to your privateRoute function, then pass them in when you export MyRoute.
const privateRoute = ({
requireLoggedIn = false,
pathIfNotLoggedIn = '/a/sensible/default',
// ...
}) // ...
Then, if you want to override the default path, you change your export to:
export default privateRoute({
requireLoggedIn: true,
pathIfNotLoggedIn: '/a/specific/page'
})(MyRoute)
The route is responsible
If you want to be able to pass in the path from the routing, you'll want to receive props for these in Private
class Private extends Component {
componentDidMount() {
const {
userStatus: {
isLoggedIn,
isOnboarded,
isWaitlisted
},
pathIfNotLoggedIn,
pathIfNotOnboarded,
pathIfNotWaitlisted
} = this.props
if (requireLoggedIn && !isLoggedIn) {
// redirect to `pathIfNotLoggedIn`
} else if (requireOnboarded && !isOnboarded) {
// redirect to `pathIfNotOnboarded`
} else if (requireWaitlisted && !isWaitlisted) {
// redirect to `pathIfNotWaitlisted`
}
}
render() {
const {
userStatus: {
isLoggedIn,
isOnboarded,
isWaitlisted
},
// we don't care about these for rendering, but we don't want to pass them to WrappedComponent
pathIfNotLoggedIn,
pathIfNotOnboarded,
pathIfNotWaitlisted,
...passThroughProps
} = this.props
if (
(requireLoggedIn && !isLoggedIn) ||
(requireOnboarded && !isOnboarded) ||
(requireWaitlisted && !isWaitlisted)
) {
return null
}
return (
<WrappedComponent {...passThroughProps} />
)
}
}
Private.propTypes = {
pathIfNotLoggedIn: PropTypes.string
}
Private.defaultProps = {
pathIfNotLoggedIn: '/a/sensible/default'
}
Then your route can be rewritten to:
<Route path="/" render={props => <MyPrivateComponent {...props} pathIfNotLoggedIn="/a/specific/path" />} />
Combine options 2 & 3
(This is the approach that I like to use)
You can also let the component and the route choose who is responsible. You just need to add the privateRoute params for paths like we did for letting the component decide. Then use those values as your defaultProps as we did when the route was responsible.
This gives you the flexibility of deciding as you go. Just note that passing routes as props will take precedence over passing from the component into the HOC.
All together now
Here's a snippet combining all the concepts from above for a final take on the HOC:
const privateRoute = ({
requireLoggedIn = false,
requireOnboarded = false,
requireWaitlisted = false,
pathIfNotLoggedIn = '/login',
pathIfNotOnboarded = '/onboarding',
pathIfNotWaitlisted = '/waitlist'
} = {}) => WrappedComponent => {
class Private extends Component {
componentDidMount() {
const {
userStatus: {
isLoggedIn,
isOnboarded,
isWaitlisted
},
pathIfNotLoggedIn,
pathIfNotOnboarded,
pathIfNotWaitlisted
} = this.props
if (requireLoggedIn && !isLoggedIn) {
// redirect to `pathIfNotLoggedIn`
} else if (requireOnboarded && !isOnboarded) {
// redirect to `pathIfNotOnboarded`
} else if (requireWaitlisted && !isWaitlisted) {
// redirect to `pathIfNotWaitlisted`
}
}
render() {
const {
userStatus: {
isLoggedIn,
isOnboarded,
isWaitlisted
},
pathIfNotLoggedIn,
pathIfNotOnboarded,
pathIfNotWaitlisted,
...passThroughProps
} = this.props
if (
(requireLoggedIn && !isLoggedIn) ||
(requireOnboarded && !isOnboarded) ||
(requireWaitlisted && !isWaitlisted)
) {
return null
}
return (
<WrappedComponent {...passThroughProps} />
)
}
}
Private.propTypes = {
pathIfNotLoggedIn: PropTypes.string,
pathIfNotOnboarded: PropTypes.string,
pathIfNotWaitlisted: PropTypes.string
}
Private.defaultProps = {
pathIfNotLoggedIn,
pathIfNotOnboarded,
pathIfNotWaitlisted
}
Private.displayName = `Private(${
WrappedComponent.displayName ||
WrappedComponent.name ||
'Component'
})`
hoistNonReactStatics(Private, WrappedComponent)
return graphql(gql`
query ...
`)(Private)
}
export default privateRoute
I'm using hoist-non-react-statics as suggested in the official documentation.
I personnaly use to build my private routes like this :
const renderMergedProps = (component, ...rest) => {
const finalProps = Object.assign({}, ...rest);
return React.createElement(component, finalProps);
};
const PrivateRoute = ({
component, redirectTo, path, ...rest
}) => (
<Route
{...rest}
render={routeProps =>
(loggedIn() ? (
renderMergedProps(component, routeProps, rest)
) : (
<Redirect to={redirectTo} from={path} />
))
}
/>
);
In this case, loggedIn() is a simple function that return true if user is logged (depends on how you handle the user session), you can create each of your private route like this.
Then you can use it in a Switch :
<Switch>
<Route path="/login" name="Login" component={Login} />
<PrivateRoute
path="/"
name="Home"
component={App}
redirectTo="/login"
/>
</Switch>
All subRoutes from this PrivateRoute will first need to check if user is logged in.
Last step is to nest your routes according to their required status.
I think you need to move your logic down a bit. Something like:
<Route path="/onboarding" render={renderProps=>
<CheckAuthorization authorized={OnBoardingPage} renderProps={renderProps} />
}/>
You will have to use ApolloClient without 'react-graphql' HOC.
1. Get instance of ApolloClient
2. Fire query
3. While Query returns data render loading..
4. Check and Authorise a route based on data.
5. Return Appropriate Component or redirect.
This can be done in following way:
import Loadable from 'react-loadable'
import client from '...your ApolloClient instance...'
const queryPromise = client.query({
query: Storequery,
variables: {
name: context.params.sellername
}
})
const CheckedComponent = Loadable({
loading: LoadingComponent,
loader: () => new Promise((resolve)=>{
queryPromise.then(response=>{
/*
check response data and resolve appropriate component.
if matching error return redirect. */
if(response.data.userStatus.isLoggedIn){
resolve(ComponentToBeRendered)
}else{
resolve(<Redirect to={somePath}/>)
}
})
}),
})
<Route path="/onboarding" component={CheckedComponent} />
Related API reference:
https://www.apollographql.com/docs/react/reference/index.html
If you are using apollo react client you can also import Query #apollo/components and use it like so in your private route:
<Query query={fetchUserInfoQuery(moreUserInfo)}>
{({ loading, error, data: userInfo = {} }: any) => {
const isNotAuthenticated = !loading && (isEmpty(userInfo) || !userInfo.whoAmI);
if (isNotAuthenticated || error) {
return <Redirect to={RoutesPaths.Login} />;
}
const { whoAmI } = userInfo;
return <Component user={whoAmI} {...renderProps} />;
}}
</Query>
where isEmpty is just checking if the given object is empty:
const isEmpty = (object: any) => object && Object.keys(object).length === 0
How can react-router properly handle 404 pages for dynamic content in a Universal app?
Let's say I want to display a user page with a route like '/user/:userId'. I would have a config like this:
<Route path="/">
<Route path="user/:userId" component={UserPage} />
<Route path="*" component={NotFound} status={404} />
</Route>
If I request /user/valid-user-id, I get the user page.
If I request /foo, I get a proper 404.
But what if I request /user/invalid-user-id. When fetching the data for the user, I will realize that this user does not exist. So, the correct thing to do seams to be:
Display the 404 page
Return a 404 http code (for server side
rendering)
Keep the url as is (I don't want a redirect)
How do I do that?? It seams like a very standard behaviour. I'm surprised not to find any example...
Edit:
Seams like I'm not the only one to struggle with it. Something like this would help a lot: https://github.com/ReactTraining/react-router/pull/3098
As my app won't go live any time soon, I decided to wait to see what the next react-router version has to offer...
First of create a middleware function for the onEnter callback, so that this is workable for redux promises:
import { Router, Route, browserHistory, createRoutes } from "react-router";
function mixStoreToRoutes(routes) {
return routes && routes.map(route => ({
...route,
childRoutes: mixStoreToRoutes(route.childRoutes),
onEnter: route.onEnter && function (props, replaceState, cb) {
route.onEnter(store.dispatch, props, replaceState)
.then(() => {
cb(null)
})
.catch(cb)
}
}));
}
const rawRoutes = <Route path="/">
<Route path="user/:userId" component={UserPage} onEnter={userResolve.fetchUser} />
<Route path="*" component={NotFound} status={404} />
</Route>
Now in this onEnter function you can work directly with the redux store. So you could dispatch an action that either successes or fails. Example:
function fetch(options) {
return (dispatch) => {
return new Promise((resolve, reject) => {
axios.get('<backend-url>')
.then(res => {
resolve(dispatch({type: `CLIENT_GET_SUCCESS`, payload: res.data}))
})
.catch(error => {
reject(dispatch({type: `CLIENT_GET_FAILED`, payload: error}));
})
}
})
}
}
let userResolve = {
fetchUser: (dispatch, props, replace) => {
return new Promise((next, reject) => {
dispatch(fetch({
user: props.params.user
}))
.then((data) => {
next()
})
.catch((error) => {
next()
})
})
}
}
Whenever the resolve promise now fails, react-router will automatically look for the next component that it could render for this endpoint, which in this case is the 404 component.
So you then wouldn't have to use replaceWith and your URL keeps retained.
If you are not using server side rendering, returning 404 before the page gets rendered would not be possible. You will need to check for the existence of the user somewhere either way (on the server or via AJAX on the client). The first would not be possible without server side rendering.
One viable approach would be to show the 404 page on error of the Promise.
I tried my solution in a project that I am making which uses Server Side Rendering and react-router and it works there, So I'll tell you what I did.
Create a function in which you'll validate an ID. If the ID is valid, Then return the with User page with proper Component, If the ID is invalid then return the with 404 Page.
See the example:
// Routes.jsx
function ValidateID(ID) {
if(ID === GOOD_ID) {
return (
<Route path="/">
<Route path="user/:userId" component={UserPage} />
<Route path="*" component={NotFound} status={404} />
</Route>
);
} else {
return (
<Route path="/">
<Route path="user/:userId" status={404} component={Page404} />
<Route path="*" component={NotFound} status={404} />
</Route>
);
}
// Router.jsx
<Router route={ValidateID(ID)} history={browserHistory}></Router>
This should work with Server Side rendering as it did in my project. It does not uses Redux.
In case of dynamic paths, you can do it like this and you don't have to change the current path.
just import the error404 component and define a property(notfound) in the state to use for conditioning.
import React, { Component } from 'react';
import axios from 'axios';
import Error404 from './Error404';
export default class Details extends Component {
constructor(props) {
super(props)
this.state = {
project: {}, notfound: false
}
}
componentDidMount() {
this.fetchDetails()
}
fetchDetails = () => {
let component = this;
let apiurl = `/restapi/projects/${this.props.match.params.id}`;
axios.get(apiurl).then(function (response) {
component.setState({ project: response.data })
}).catch(function (error) {
component.setState({ notfound: true })
})
}
render() {
let project = this.state.project;
return (
this.state.notfound ? <Error404 /> : (
<div>
{project.title}
</div>
)
)
}
}
I encountered a similar problem while making a blog website. I've been searching for a solution for a while now. I was mapping (using map function) my blog component based on dynamic link.
The initial code snippet was as follows:
import Blog from '../../Components/Blog/Blog.component';
import './BlogPage.styles.scss';
const BlogPage = ({ BlogData, match }) => {
return (
<div className='blog-page'>
{
BlogData.map((item, idx)=>
item.link === match.params.postId?
<Blog
key={idx}
title={item.title}
date={item.date}
image={item.image}
content={item.content}
match={match}
/>
:''
)
}
</div>
)
};
export default BlogPage;
I used a hack where I would use filter function instead of map and store it and then check if it exists (in this case check if length greater than zero for result) and if it does the blog component is rendered with the props for the page else I render the Not Found component (My404Component).
The snippet as follows:
import Blog from '../../Components/Blog/Blog.component';
import My404Component from '../../Components/My404C0mponent/My404Component.component';
import './BlogPage.styles.scss';
const BlogPage = ({ BlogData, match }) => {
const result = BlogData.filter(item => item.link === match.params.postId);
console.log(result);
return (
<div className={result.length>0? 'blog-page': ''}>
{
result.length>0?
<Blog
title={result[0].title}
date={result[0].date}
image={result[0].image}
content={result[0].content}
match={match}
/>
:<My404Component />
}
</div>
)
};
export default BlogPage;
This way the Blog component is not rendered as long as the value of the entered link is not valid as result would be an empty array and it's length would be 0 and instead My404Component would be rendered.
The code is a little raw I havn't refactored it yet.
Hope this helps.