React router doesn't work correct with component - reactjs

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 /

Related

Sharing React Component props with grandchildren components

Im struggling to figure out how to share props with grandchildren of a component in react/typescript. Why is the constructer props getting overwritten?
class Resource extends Component<IResourceProps, IResourceState> {
state:IResourceState = {
}
_finder: new this.props.finder({api: this.props.api})
render() {
return (
<div>
<Switch>
<Route path={this.props.path} component={() => {return this._finder}} />
</Switch>
</div>
)
}
}
export default Resource;
class UserFinder extends React.Component<IResourceFinderProps, IResourceFinderState> implements IResourceFinder {
_data: any[] = []
load = (response: QueryResponse): void => {
this._data = response.records;
}
constructor(props:IResourceFinderProps) {
super(props);
}
componentDidMount() {
this.props.api.query({ //<-------- this.props.api is undefined! Why?
page: 0,
pageSize: 100
}, (response:QueryResponse) => {
this.load(response);
})
}
render() {
const columns = [
{
title: 'Name',
dataIndex: 'firstName',
key: 'firstName'
}
];
return (
<div>
<Table columns={columns} rowKey="userId" dataSource={this._data} />
</div>
)
}
}
export default UserFinder
App.js
<Route path="/users" finder={UserFinder} api={this.api} />
Essentially, I want to share the api prop with all of the children of the Route without having to explicitly set it on each one individually. Perhaps, another solution could be giving the child a reference to the grandparent somehow. What are some of my options?
First, if you just want to pass props through the Router, use the prop render instead of component. E.g.:
<Route path={this.props.path} render={(props) => <Component {...props} additionalProps={...} />} />
If you really want to share state without passing props, you will need something like a global state. There, you have different options.
The formerly most used was redux (see docs and well known tutorial ).
With the React update and the support of hooks, there are simpler ways to build such a global state system. You could build it from scratch with useContext & useReducer like this
Or you could just use ReactN which gives you a global state out of the box (see here)

React HOC infinite loop when applied in routing

I'm encountering an infinite loop when trying to render my component routes with a HOC wrapper. The routing render method looks something like:
const render = (route, additionalProps) => {
const scrollActive = has(route, "scroll") ? route.scroll : true;
const Component = scrollActive
? withScrollToTop(route.component)
: route.component;
return props => {
return <Component {...props} {...additionalProps} route={route} />;
};
return (
<Switch>
{routes.map((route, i) => {
return (
<Route
key={i}
path={route.path}
exact={route.exact}
strict={route.strict}
render={render(route, injectProps)}
/>
);
})}
</Switch>
);
For the time-being (since it's looping out), the HOC isn't actually doing anything besides wrapping the component with another class.
export default function withScrollToTop(WrappedComponent) {
const displayName = `HigherOrder.WithScrollToTop('${getDisplayName(
WrappedComponent
)}')`;
class WithScrollToTop extends React.PureComponent {
static displayName = displayName;
static WrappedComponent = WrappedComponent;
render() {
const props = Object.assign({}, this.props);
return React.createElement(WrappedComponent, props);
}
}
return hoistStatics(WithScrollToTop, WrappedComponent);
}
Attempting to hit any route causes an infinite loop.
Your problem is this line:
render={render(route, injectProps)} (yours do not have the final }).
This will trigger render function, it does not pass the function to children props.

react-router: Not found (404) for dynamic content?

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.

Can the url params of the react-router <Route /> component be validated?

I'm using react-router (2.4.1) and I have a wildcard (path="*") fallback route to display a "Not Found" component.
One of my routes are defined like:
<Route path="/campaigns/:category" component={CampaignsPage} />
Can I validate the category route param in any way to check if this category exist in my list of categories? and if not, I want the router to fall down to the wildcard in order to display the Not Found component
Maybe you could validate the received param inside the 'campaigns' component? Something along the lines of:
class CampaignsPage extends Component {
render() {
const validCampaignCats = [...]
const receivedCampaignCat = this.props.params.category
const renderChild = (actualCat, validCats) => {
if (!validCats.includes(actualCat)) {
return <YourErrorPage/>
}
return this.props.children
}
return (
<div>
<Header/>
{ renderChild(receivedCampaignCat, validCampaignCats) }
<Footer/>
</div>
)
}
}
Hope that helps!

Get the current route name into the component

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>;
}
}

Resources