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
Related
I'm trying to figure out why the component SubPage is not rendering whenever the route path of /sub/:_id is visited (e.g. /sub/5f1c54257ceb10816a13d999). This is the first time I've worked with react routes. The :_id part should presumably accept query parameters from the URL dynamically so I cannot see why this is not working.
I can get the /subs page to fetch the API and render each sub on the page but just not each individual sub page.
The route is as follows near the bottom of App.js: <Route path={"/sub/:_id"} component={SubPage} />
Thanks for any help here. I've made a stackblitz for convenience, or you can see the relevant code below:
And subPage.js:
import React from 'react'
export class SubPage extends React.Component {
render() {
return (
<div className="sub-details-individual">
<h1 class="std-intro">Viewing a Single Subscriber</h1>
<div className="sub-specs">
<div className="sub-specs-inner">
id: {this.props.params._id}
</div>
</div>
</div>
)
}
}
And App.js:
import React, {Component} from "react";
import {navLinks} from "./components/nav-links";
import Root from "./components/Root";
import {
BrowserRouter as Router,
Switch,
Route
} from "react-router-dom";
import {SubPage} from "./components/subPage";
import ShowSubs from "./components/show-subs";
export default class App extends Component {
constructor() {
super();
this.state = {
navLinks: navLinks,
intro: "hello world",
url: "someurl"
}
}
updateURL = (newUrl) => {
this.setState({
url: newUrl
})
}
render() {
return (
<Router>
<Root navLinks={this.state.navLinks} intro={this.state.intro}></Root>
<Switch>
<Route path="/subs">
<p>subs page</p>
{/*this.updateURL('/subs') fails presumably because it causes the rerender infinitely - but how to solve?*/}
<ShowSubs />
</Route>
<Route path="/">
<p>homepage</p>
</Route>
<Route path={"/sub/:_id"} component={SubPage} />
</Switch>
<p>the url: {this.state.url}</p>
</Router>
);
}
}
Two things:
this.props.params._id will crash since you are missing match before params
this.props.match.params._id
few exact props are missing, especially in the subs path:
<Route exact path="/subs">
Note: the exact prop will be useful in the / route as well.
I have this App.jsx that routes to (renders) different components.
But I have set <NavigationBar /> and an h1 tag between Router and Switch because I need to render those two components for every page.
So now what I want is to get the current route name/path name that displays on the browser address bar. This path is changing when I click on different links (Link) to render different components.
But the path value is the same / even though the path is changing for every Link click.
I even used componentDidUpdate but it didn't work as it gave the error
maximum update depth exceeded componentdidupdate
this is my App.jsx
import React, { Component } from "react";
import "./App.css";
import "./css/custom.css";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import NavigationBar from "./pages/homepage-components/1-navbar";
import HomePage from "./pages/HomePage";
import Post from "./pages/Post";
class App extends Component {
constructor(props) {
super(props);
this.state = {
currentPath: "",
};
}
componentDidMount() {
this.setState({
currentPath: window.location.pathname,
});
}
render() {
return (
<Router>
{/* --------------- Navigation Bar --------------- */}
<NavigationBar />
<h1>Path is: {this.state.currentPath}</h1>
{/* --------------- End of Navigation Bar --------------- */}
<Switch>
<Route exact path="/" component={HomePage} />
<Route path="/post" component={Post} />
</Switch>
</Router>
);
}
}
export default App;
Even though the different components are rendered as path changes, the value for this.state.currentPath doesn't update.
Can someone help, please?
useLocation hook provides current location:
function NavigationHeader() {
const location = useLocation();
return <h1>Path is: {location.pathname}</h1>;
}
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
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>
)
I have these scenarios
Settings Page -> Results Page -> Details Page
User chooses settings, clicks next, gets results and then clicks into more details.
Details Page -> Results Page
User goes back to Results page from Details Page. This causes a full re-render, causing me to hit the server again for no point(I have the results stored in an array).
Details Page -> Results Page -> Settings Page -> Results Page
The user goes to details, then back to results(want to grab stored results), then goes back to settings page, makes changes and then goes back to results page(now I want a full grab from server again).
I am wondering if there is away in react router to determine if I came to the page via the browser history or if I was going in a forward motion.
I was looking for the same thing then I finally find a solution that seems simple.
Quick answer:
I use history.action value to determine if the user is coming from a back button or from a classic navigation.
If history.action is equal to 'POP' then the back button has been hit. Otherwise it's 'PUSH'.
Detailed answer:
I get access to history in props of each page because I do something like that:
<Provider store = { store }>
<AppRouter />
</Provider>
Where AppRouter is:
import React from 'react';
import { Router, Route, Switch } from 'react-router-dom';
import createHistory from 'history/createBrowserHistory';
import PublicRoute from './PublicRoute';
import PageHome from '../pages/front/home/PageHome';
export const history = createHistory();
const AppRouter = () => (
<Router history={ history }>
<div>
<Switch>
<PublicRoute
exact = { true }
path = "/"
component = { PageHome }
history={ history }
/>
< ..... Other routes here ..... >
</Switch>
</div>
</Router>
);
export default AppRouter;
And PublicRoute component is something like that:
import React from 'react';
import { Route } from 'react-router-dom';
import Header from '../components/header/Header';
const PublicRoute = ( { component: Component, ...rest } ) => (
<Route
component={ ( props ) => (
<div>
<Header />
<Component { ...props } />
</div>
)}
{ ...rest }
/>
);
export default PublicRoute;
In React Router, the component stays mounted if router calls for paths that are children of that component. So, in your example, you can do something like the following:
<Route path="items" component={Results}>
<Route path=":id" component={Detail} />
</Route>
This way, Results component does not get unmounted when Detail component is being mounted because Detail is a child of Results. However, if you do not want to see Results component getting rendered when you are in Detail, you can only render children when they exist. Something like the following:
class Results extends React.Component {
render() {
if (this.props.children) {
// This will enter Detail component
return this.props.children;
}
return (
// Render results component
);
}
}