React Router V4 not updating content on optional parameter change - reactjs

I am new to React and I am working on the search functionality of a site. I am using a create-react-app base.
Here is the routing:
<BrowserRouter>
<App>
<Switch>
<Route path='/' exact component={Home} />
<Route path='/blah' component={Blah} />
<Route path='/boop' component={Boop} />
<Route path="/search/:searchTerm?" component={Search} />
<Route path="*" component={NotFound} />
</Switch>
</App>
</BrowserRouter>
Note the search one has an optional parameter. I am picking up this parameter fine in Search.js
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import SearchResults from './SearchResults';
export default class Search extends Component {
constructor(props) {
super(props);
this.state = {
searchTerm: this.props.match.params.searchTerm
}
}
render() {
return (
<div className="Search">
SEARCH: {this.state.searchTerm}
<SearchResults searchTerm={this.state.searchTerm} />
</div>
);
}
}
Over in the actual form that triggers the search, this snippet handles the click:
handleClick(event) {
this.props.history.push('/search/'+this.state.value);
}
This works and displays results for paths like /search/test and /search/woo. The only problem is when I go directly from one search to another.
/search/test -> /search/woo updates the path in the address bar, but does not render the content of the woo page with the new search results.
Going to another path between makes it work. So /search/test -> /boop -> /search/woo all works as expected.
Am I missing something here? Do I need to manually trigger this somehow, or should optional parameter changes trigger the components to update?
Thanks!

You need to sync state to props on every change if you want to store this term in component's state.
export default class Search extends Component {
...
componentWillReceiveProps(nextProps) {
const newSearchTerm = nextProps.match.params.searchTerm
if(this.state.searchTerm !== newSearchTerm) {
this.setState(() => ({ searchTerm: newSearchTerm }))
}
}
...
}
But there is no reason to do it. Use this.props in render. It could be as simple as this
export default ({ match: { params: { searchTerm } } }) => (
<div className="Search">
SEARCH: {searchTerm}
<SearchResults searchTerm={searchTerm} />
</div>
)

Related

Why is PrivateRoute preventing my Redux-connected component from re-rendering on store state update?

In my <Content> component I have:
<PrivateRoute path="/monitors" component={MonitorsPage}/>
and within <MonitorsPage>:
<Route path="/monitors/:device_id/live" component={MonitorLive}/>
<MonitorsLive> uses Redux connect() to subscribe to store state changes.
Here is my test <PrivateRoute>:
import React from "react";
import { Route } from "react-router-dom";
function delay(t, v) {
return new Promise(function(resolve) {
setTimeout(resolve.bind(null, v), t)
});
}
class PrivateRoute extends React.Component {
state = {
isLoaded: false,
};
authenticate() {
delay(1000).then(()=>{
console.log('AUTHENTICATED');
this.setState({isLoaded: true})
})
}
componentDidMount() {
this.authenticate()
}
render() {
const {component: Component, ...rest} = this.props;
const { isLoaded } = this.state;
return (
<Route {...rest} render={
props => (!isLoaded ? <div>loading...</div> : <Component {...props} />)
}
/>
)}}
export default PrivateRoute
If I navigate to the /monitors/:device_id/live route and refresh browser, the component loads and mounts fine, but fails to re-render on store state changes in this condition. It works fine under a number of other conditions, including:
Navigating to the problem route from the /monitors route (instead of browser hard-reload) OR
<Content> and <MonitorsPage> both use <Route> instead of <PrivateRoute> OR
<Content> and <MonitorsPage> both use <PrivateRoute> instead of <Route> OR
<Content> uses <Route> and <MonitorsPage> uses <PrivateRoute> OR
this.setState({isLoaded: true}) is executed with no preceding delay(1000) in PrivateRoute
How can I make this so I can place my PrivateRoute as a parent when I know all children Routes are also going to be private, without breaking redux?
UPDATE: I've modified MonitorLive and MonitorsPage to include withRouter() in the export statement. Content already had it. This change doesn't resolve the issue. Example export statement:
export default withRouter(connect(mapStateToProps,mapDispatchToProps)(MonitorLive))
UPDATE 2: In addition to the 5 circumstances which eliminate the issue mentioned above, there's this:
If I remove either the line with "text" or <span>span</span> from Content:
class Content extends Component {
render() {
return (
<div>
<div>
text
<span>span</span>
</div>
<div>
<button onClick={this.props.getMonitorLiveValues}>Update State</button>
<Switch>
<PrivateRoute
path="/monitors"
component={MonitorsPage}
/>
</Switch>
</div>
</div>
)}}
UPDATE 3:
Demonstrates the issue: https://vvmk3qorq7.codesandbox.io/monitors/ARMS-NVM-P5/live
https://codesandbox.io/s/vvmk3qorq7
The issue is resolved by upgrading react-dom from 16.3.2 to 16.4.0

How to re-render the same component being used in different routes?

I have several routes rendering the same component. Depending on the route I want the component to fetch different data. However since I keep rendering the same component, React doesn't see any changes to the DOM when I click a Link tag (from my nav bar located in the Layout component) to another route rendering that same component. Meaning the component is not re-rendered with the new data. Here are my routes:
class App extends Component {
render() {
return (
<BrowserRouter>
<Provider store={store}>
<Layout>
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/fashion" component={PostTypePageContainer} />
<Route exact path="/beauty" component={PostTypePageContainer} />
</Switch>
</Layout>
</Provider>
</BrowserRouter>
);
}
}
export default App;
Here is the PostTypePageContainer component that I want to re-render with the new data each time:
class PostTypePageContainer extends Component {
componentDidMount() {
let route;
switch (this.props.location.pathname) {
case '/fashion':
route = '/fashion';
break;
case '/beauty':
route = '/beauty';
break;
default:
console.log('No data was found');
}
let dataURL = `http://localhost:8888/my-site//wp-json/wp/v2${route}?_embed`;
fetch(dataURL)
.then(res => res.json())
.then(res => {
this.props.dispatch(getData(res));
});
}
render() {
let posts = this.props.postData.map((post, i) => {
return <PostTypePage key={i} props={post} />;
});
return <div>{posts}</div>;
}
}
const mapStateToProps = ({ data }) => ({
postData: data.postData
});
export default connect(mapStateToProps)(PostTypePageContainer);
How do I go about re-rendering that component each time?
This is intended behavior of react-router.
While i suggest you create a HOC to fetch the data from different locations and pass it to the PostTypePageContainer via props, using a key will give you a quick work around that will cause your component to remount.
class App extends Component {
render() {
return (
<BrowserRouter>
<Provider store={store}>
<Layout>
<Switch>
<Route exact path="/" component={Home} />
<Route exact key={uniqueKey} path="/fashion" component={PostTypePageContainer} />
<Route exact key={someOtherUniqueKey} path="/beauty" component={PostTypePageContainer} />
</Switch>
</Layout>
</Provider>
</BrowserRouter>
);
}
}
export default App;
Source: https://github.com/ReactTraining/react-router/issues/1703
I wasn't able to get the <Route key={...} ... /> to work in my case. After trying several different approaches the one that worked for me was using the componentWillReceiveProps function in the reused component. This was getting called each time the component was called from a <Route />
In my code, I did:
componentWillReceiveProps(nextProps, nextContext) {
// When we receive a call with a new tag, update the current
// tag and refresh the content
this.tag = nextProps.match.params.tag;
this.getPostsByTag(this.tag);
}

React router 4 not updating when using children prop on history.push

I have this piece of code:
class Base extends Component {
changeRoute = () => {
// after this, address bar gets updated but I can't see the Users
// component, only Home component.
this.props.history.push('/users');
}
render() {
return (
<div>
<MyBar />
{this.props.children}
</div>
)
}
}
class App extends Component {
render() {
return (
<Router>
<Base>
<a onClick={this.changeRoute}>Change</a>
<Route path="/home" component={Home} />
<Route path="/users" component={Users} />
</Base>
</Router>
)
}
}
However, when I try to change location by either using history.push or push from react-router-redux I can see the browser path updated but the content is not updating, it shows the original path content. I also noted that whenever I refresh the browser I can see the correct content related to the path.
When I change the code to the following, both the browser path and the content updates accordingly as expected, my question is: Why is it behaving different? if in my opinion both codes do the same thing.
class Base extends Component {
render() {
return (
<MyBar />
)
}
}
class App extends Component {
changeRoute = () => {
// after this, address bar and component are updated as expected.
this.props.history.push('/users');
}
render() {
return (
<Router>
<div>
<Base />
<a onClick={this.changeRoute}>Change</a>
<Route path="/home" component={Home} />
<Route path="/users" component={Users} />
</div>
</Router>
)
}
}
Edit:
I added to the source code the push method. This is the piece of code I'm using to export the App component:
import {withRouter} from 'react-router';
export default withRouter(App);
This is the code I ran and it is running fine. Can you please check whether there is anything I need to change in my code to reproduce your issue?
import React from 'react';
import ReactDOM from 'react-dom';
import {NavLink, BrowserRouter,Route} from 'react-router-dom';
class Base extends React.Component {
render() {
return (
<div>
<div>MyBar</div>
{this.props.children}
</div>
)
}
}
class App extends React.Component {
render() {
return (
<BrowserRouter>
<Base>
<NavLink to="/users">Change</NavLink>
<Route path="/home" render={()=>"Home"} />
<Route path="/users" render={()=>"Users"} />
</Base>
</BrowserRouter>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'));
Edit: From what I understood, you want to be able to navigate to the user page when the change link is clicked. The correct way to do it is using the NavLink control. This would not require the use of withRouter HOC which should only be used inside a Router component and not outside it.

React routing - Keeping props within the same view

I currently have a few routing paths in my code which pass in a number of properties to those views. As below:
import React, {Component} from 'react'
import { Route, Switch, Redirect, withRouter } from 'react-router-dom'
import CalendarView from '../calendarView'
import ListView from '../listView'
import AgendaView from '../agendaView'
import propTypes from 'prop-types'
class DiaryRouting extends Component{
render() {
const activities = this.props.activities
return (
<switch>
<Route exact path="/my-diary/" render={() => <Redirect push to="/my-diary/activities/calendar-view/month" component={(props) => <CalendarView {...props} selectedViewRange = 'calendar-view' selectedViewType='month' selectedDiaryType='activities' activities={activities}/>} /> } />
<Route exact path="/my-diary/activities/" render={() => <Redirect push to="/my-diary/activities/calendar-view/month" component={(props) => <CalendarView {...props} selectedViewRange = 'calendar-view' selectedViewType='month' selectedDiaryType='activities' activities={activities}/>} /> } />
<Route exact path="/my-diary/jobs/" render={() => <Redirect push to="/my-diary/jobs/calendar-view/month" component={(props) => <CalendarView {...props} selectedViewRange = 'calendar-view' selectedViewType='month' selectedDiaryType='jobs' activities={activities}/>} /> } />
</switch>
)
}
}
DiaryRouting.propTypes = {
activities: propTypes.array,
}
export default DiaryRouting
I have items being passed in such as selectedViewRange and selectedDiaryType into each route. What I am trying to accomplish to having a variable in this view that holds what route it has gone through and what variable has been passed in. Such as below:
....
state = {
selectedViewRange: null
}
... <Route exact path="/my-diary/activities/"
render={() => <Redirect push to="/my-diary/activities/calendar-view/month"
component={(props) => this.setState(selectedViewRange: 'calendar-view') <CalendarView {...props} selectedViewRange = 'calendar-view' selectedViewType='month' selectedDiaryType='activities' activities={activities}/>} /> } />
However I keep getting a
Warning: setState(…): Cannot update during an existing state
transition
I have tried using a variable instead of a state and that does not do anything.
Best approach how to tackle this?
First off, I'd strongly suggest to use redux for app state management because setState has never been a reliable and scaleable choice.
Anyways, to answer your question:
For the class component that holds your state for activeRoute (rephased your selectedViewRange for better understanding), you should define a method like:
setActiveRoute = (activeRoute) => { this.setState({ activeRoute }) }
Pass it as a prop to the components that you render in your routes.
So basically, your CalendarView should have an extra prop like setActiveRouteName and you should pass this.setActiveRoute as a callback to setActiveRouteName prop like:
<CalendarView {...props} setActiveRouteName={this.setActiveRoute} ... />
Moving on, in your CalendarView, declare a React lifecycle method componentDidMount. It should look like:
export default class CalendarView extends Component {
componentDidMount(){
this.props.setActiveRouteName('yourRouteName');
}
...other code...
}
This should solve the problem.

Implement search bar using ReactJS

I am building a beginner React app and am not able to understand how to handle my state so that I can redirect to a search results page:
I have a main App component which uses React Router to deliver two components:
1) Landing (/) -- has an input and should take you to /search and show only those objects whose title match your input
2) Search (/search) -- either shows all objects if accessing the page directly or your filtered based upon your input
My question is: if I handle the state in the App component, it will cause the state to update and a rerender upon a user typing in the Landing input element, but how can I get it to go to /search with the updated state? The index route will keep getting hit since it's just a rerender and the user is still on the landing page.
I would like to handle this without redux as this will be a very small app.
Here is the code for my parent component:
import React, { Component } from "react";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import { shape, string } from "prop-types";
import Landing from "./Landing";
import Search from "./Search";
import { shows } from "../data.json";
class App extends Component {
constructor(props) {
super(props);
this.state = {
searchTerm: ""
};
this.updateSearchTermHandler = this.updateSearchTermHandler.bind(this);
}
updateSearchTermHandler(searchTerm) {
this.setState({ searchTerm });
}
render() {
return (
<BrowserRouter>
<div className="app">
<Switch>
<Route
exact
path="/"
component={props => (
<Landing
updateSearchTermHandler={this.updateSearchTermHandler}
searchTerm={this.state.searchTerm}
{...props}
/>
)}
/>
<Route
path="/search"
component={props => (
<Search
updateSearchTermHandler={this.updateSearchTermHandler}
shows={shows}
{...props}
/>
)}
/>
</Switch>
</div>
</BrowserRouter>
);
}
}
App.propTypes = {
match: shape({
params: string.isRequired
}).isRequired
};
export default App;
One potential solution is to instead use a <Router> with your own history. You could then call history.replace('/search', { searchTerm: 'foo' })
And then in your Landing component, you will have this.props.history.location.state.searchTerm
See https://reacttraining.com/react-router/web/api/Router for further details on creating history

Resources