How to reload a component in reacts on url parameter change? - reactjs

I have the following route:-
ReactDOM.render((
<Router history={browserHistory}>
<Route path="/" component={app}>
<IndexRoute component={home}/>
<Route path="/articles" component={articles} />
<Route path="/articles/topics/:featureName" component={filteredArticles} />
</Route>
</Router>
), document.getElementById('app'));
Now when i request for /articles/topics/topic1, the first 5 contents are fetched are using API call. When user reaches the bottom of the page again an API call is made where next 5 contents are fetched. This is working fine(Notice that the handlescroll function checks for the page bottom, which has few minor issues so commented that out for now.)
Now say I choose next topic from the list so the url changes, so my url changes to /articles/topics/topic2 , now i need the same thing as mentioned above and call apis for this topic.
Following is my current component code:-
import React from 'react';
import axios from 'axios';
import ArticleList from './ArticleList';
import Banner from './Banner';
import LeftNavBar from './LeftNavBar';
import RightNavbar from './RightNavbar';
import {url} from './Constants';
/*MERGE INTO ARTICLES ONCE SCROLL LOGIC IS FIXED*/
class FilteredArticles extends React.Component{
constructor(){
super();
{/* Adding global event listener,binding it to scroll event and adding it to handleScroll function */}
/*window.addEventListener("scroll", this._handleScroll.bind(this));*/
this.state ={
articles : [],
loadingFlag:false,
limit : 5,
offset :0,
subFeatureName:[],
flag: false
}
}
_getContent(){
let self=this;
axios.get(url+'/articles/filter/?filter=popular&category='+this.props.params.featureName+'&limit='+this.state.limit+'&offset='+this.state.offset)
.then(function(response){
/*Checking if limit has exceeded meta count, if it has stopped we dont call the APIS*/
if(self.state.limit >= response.data.meta.total_count){
self.setState({articles : self.state.articles.concat(response.data.results),
offset : self.state.limit,
limit: self.state.limit+self.state.offset,
subFeatureName: self.props.params.featureName,
loadingFlag: false}
)
}
else{
self.setState({articles : self.state.articles.concat(response.data.results),
offset : self.state.limit,
limit: self.state.limit+self.state.offset,
subFeatureName: self.props.params.featureName,
loadingFlag: true}
)
}
})
.catch(function(error){
console.log(error);
});
}
_handleScroll(){
/*Calling 5 more articles when someone scrolls down and reaches the page end
const windowHeight = window.innerHeight;
const scrollT = $(window).scrollTop();
if(windowHeight- scrollT < 200){
if(!this.state.loadingFlag){
this.setState({loadingFlag : true});
this._getContent();
}
}*/
}
componentDidMount(){
/*calling _getContent function to get first five articles on page load*/
console.log("got content");
this._getContent();
}
render(){
console.log("inside render");
return(
<div>
<Banner title="Article Page"/>
<div className="content-container">
<LeftNavBar/>
<div className ="feeds">
<div className="card">
{/* Sending hideSeeAll as prop to remove see all button on main article page*/}
<ArticleList articles={this.state.articles} hideSeeAll='true' keyword="Top Articles"/>
</div>
</div>
<RightNavbar/>
</div>
</div>
)
}
}
export default FilteredArticles;
So basically I am trying to understand that if I have a route like /articles/topics/:featureName and I call a page like /articles/topics/topic1 or /articles/topics/topic2, can the component be realoaded(I need to call different API are fetch content once again).
That is, for /articles/topics/topic1 the API called is
/api/v0/articles/filter/?filter=popular&category=topic1&limit=5&offset=0
and for /articles/topics/topic2 the API called is
/api/v0/articles/filter/?filter=popular&category=topic2&limit=5&offset=0
Is there any way to achieve this?

You shouldn't really be querying the API/updating the components state in this fashion but hey that's another question entirely. Maybe consider using a Flux approach.
Anyhow, you can use the componentWillReceiveProps(nextProps) life cycle method to receive the routers nextProps params and query your API again:
componentWillReceiveProps(nextProps) {
this._getContent(nextProps.params.featureName);
}
_getContent(featureName) {
...query your API
}

Also, if you're using Redux, make a check like this to avoid an infinite loop:
componentWillReceiveProps(newProps) {
if (this.props.someValue === newProps.someValue) {
this.props.fetchSomething(newProps.params.somethingName);
}
}

Related

Navigate to another page using Material SearchBar

I am using a Material SearchBar in the header of my app, and trying to navigate to a page '/search' on localhost onRequestSearch. The code for the search bar itself is simple:
<SearchBar
onChange = {() => console.log('onChange')}
onRequestSearch = {() => this.setRedirect()}
style = {{ margin: '0 auto', maxWidth:800}}/>
Now, I've tried implementing setRedirect function in a number of ways with no luck. First, I simply tried:
setRedirect() {
this.props.history.push('/search');
}
But this gives an error of Undefined is not an object (evaluating this.props.history). I do have a Route set up for "/search" page in my main app.
I also tried using Redirect:
constructor(props) {
super(props);
this.state = {
redirect = false
}
}
setRedirect() {
this.setState({
redirect: true
}
}
And in the render method:
render() {
if(this.state.redirect) {
return (
<Router>
<div>
<Redirect push to="/search"/>
<Route path="/search" exact strict component={Search}/>
</div>
</Router>
);
}
else {
// return other stuff
}
}
This simply results in the entire page going blank when I type something in the search bar and hit enter. The address in the browser does NOT change to '/search' either.
Not sure what I'm doing wrong. Simply trying to navigate to another page once user enters text in the search bar and hits enter. Any suggestions will be greatly appreciated. Thank you in advance!
Try to add withRouter HoC and then try again with this.props.history.push()
import { withRouter } from 'react-router-dom'
...
export default withRouter(MyComponent);

Unable to prevent rerender with shouldComponentUpdate() or with PureComponent

In /home/:id I have a <LogButtons/> when its clicked logOn() get called so logsignPopUp variable become a <logForm/> component.
In the same page I have a <IframeYoutubeComponent/>, I want to prevent it to rerender when the <logForm/> pop on the screen so the video isn't reloaded.
home.js :
export default class Home extends Component {
constructor(props) {
super(props);
this.state = { logsign: "" };
this.logOn = this.logOn.bind(this);
this.signOn = this.signOn.bind(this);
}
logOn() {
this.setState({ logsign: "log" });
}
render() {
let logsignPopUp = this.props.logsign === "log" ? <LogForm/> : this.state.logsign;
let homePage =
<div>
{logsignPopUp}
<div>
<LogButtons logOn={this.logOn}/>
</div>
<div>
<IframeYoutubeComponent paramsId={this.props.match.params.paramsId}/>
</div>
</div>;
return (
<div>
<Route exact path="/home/:id" component={() => <div>{homePage}</div> } />
</div>
);
}
}
iframeYoutubeComponent.js :
export class IframYoutubeComponent extends Component {
render() {
//this.props.youtube come from Redux state
let src = this.props.youtube.find(el => el.youtubeId === this.props.paramsId);
return (
<iframe src={"https://www.youtube.com/embed/" + src}></iframe>
);
}
}
I tried to return false in shouldComponentUpdate() but its not even called :
shouldComponentUpdate() {
console.log("test");
return false;
}
I tried to use a PureComponent for <IframeYoutubeComponent/> but the video still reload when the <logForm/> pop.
I tried to add key to my components and also tried to put this.state.logsign in Redux but nothing worked out.
I started react since 2 months so I might miss something obvious but I can't find out what... Any idea ?
That's because you are passing an arrow function in a component prop to the Route. This way everytime you generate a new function.
You should pass a react component in this prop or at least a function that returns JSX but this function should be defined once. For example as a class method.
Something like:
<div>
<Route exact path="/home/:id" component={this.renderHomePage} />
</div>
But then of course you have to refactor your logic regarding this logsign prop.

React-router and redux manual URL enter error

i have a problem of undefined props from redux store.
here is my routeHandler file
function organisationsFromStore(store) {
const { router, organisations } = store;
return {
organisations
}
}
function organisationFromStore(store) {
const { router, organisations } = store;
const { organisationId } = router.params;
return {
organisation: organisations.get(parseInt(organisationId))
}
}
export const organisationRouteHandler = connect(organisationFromStore)(Organisation);
export const accountsConfigurationRouteHandler = connect(organisationsFromStore)(AccountsConfiguration);
This is hooked to my getRoutes.jsx file which handles routes:
<Route path="accounts" component={accountsConfigurationRouteHandler}>
<Route path=":organisationId" component={organisationRouteHandler}></Route>
</Route>
In my Organisation.jsx(which gets organisations prop from it's parent AccountsConfiguration) component i have:
render() {
return (
<div style={{display: "inline"}}>
<div className={styles.desks}>
<span className={deskStyles.desksLabel}>Desks</span>
<ul className={deskStyles.desksList}>
{
this.props.organisation.get("desks").map(desk => {
return <li>{desk.get("name")}</li>;
})
}
</ul>
</div>
<div className={this.state.childrenStyle}>
{this.props.children}
</div>
</div>
);
When i thy to enter my URL manually e.g localhost:1234/accounts/4 (4 being the organisationId), i get the error saying this.props.organisations is not defined, which breaks the app. This is happening because first route's handler (organisationsFromStore) did not store the organisations and it didn't pass it as prop to AccountsConfiguration, which then didn't pass it to Organisations via this.props.children.
What is the best way to make the component wait for all the previous routes to get their props, and then render in this case Organisation component without error? Or is there a better way of doing this. Hope i was clear, thanks.
P.S I'm using old redux-router version before v2, and it must be that version at this time.
Ok, my mind wasn't working properly yesterday, i just inserted check if props are defined and rendered it conditionaly this.props.organisation ? .... : null

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.

Child route is blocking the parent route`s render

I have a react app which is using react-router. I`m using plain routes, but this is how components represent my routing
<Routes>
<Route component={CoreLayout}>
<Route component={AppLayout}
onEnter={fetchData}>
<Route path='dashboard'
onEnter={fetchStatistics}
component={Dashboard}>
</Route>
</Route>
</Routes>
The situation now
First, the app layout is going to block every render while it is fetching the necessary data from the server (like the User data). Then if we have the data, we can step on the child routes, like in this case the Dashboard route, where we are loading the content of the pages.
The goal
The problem is whit this strategy, we are going to show a blank white page until the onEnter on the main route is resolved.
To avoid this, I would like to load the AppLayout component, but without starting the onEnter function on the child route. To do this, I can show a waiting spinner where the child component would load, and when the data is loaded I can start loading the child`s data.
tl;dr
The question is, how can I make the parent layout to render, while the child route`s onEnter is not loaded.
Instead of using onEnter, you can have your <Dashboard> initiate its data fetching in its component(Will|Did)Mount method. Have it maintain a state.loaded boolean which displays a spinner when state.loaded = false.
class Dashboard extends React.Component {
constructor(props) {
super(props)
this.state = {
loaded: false
}
}
componentWillMount() {
// mock data fetch call that uses a promise
fetchStatistics()
.then(resp => resp.json())
.then(data => {
this.setState({
loaded: true,
data
})
})
}
render() {
// if data hasn't been loaded, render a spinner
if (!this.state.loaded) {
return <Spinner />
}
// data has been loaded, render the dashboard
return (
<div>...</div>
)
}
}
Edit:
It doesn't handle data loading errors, but here is an example of a general purpose data loading HOC that should work (haven't tested it):
/*
* #Component is the component to render
* #fetchData is a function which fetches the data. It takes
* a callback function to trigger once data fetching has
* completed.
*/
const withData = (Component, fetchData) => {
return class WithData extends React.Component {
constructor(props) {
super(props)
this.state = {
loaded: false
}
}
componentWillMount() {
this.props.fetchData(() => {
this.setState({ loaded: true })
})
}
render() {
return this.state.loaded ? (
<Component {...this.props} />
) : (
<Spinner />
)
}
}
}
Usage
function fetchStatistics(callback) {
fetch('/api/dashboard')
.then(resp => resp.json())
.then(data => {
dispatch(dashboardData(data))
callback()
})
})
<Route
path='dashboard'
component={withData(Dashboard, fetchStatistics} />

Resources