I'm trying to get the current path of the react router in a container so I can pass it to a child component that will change it's visibility filter.
More specifically, I'm trying to make a navigation menu highlight the currently active page.
I'm using react, redux, react-router, and react-router-redux so I can access the router state from the redux store.
From the docs for react-router-redux, it says to do something like this:
function mapStateToProps(state, ownProps) {
return {
id: ownProps.params.id,
filter: ownProps.location.query.filter
};
}
Here is my container component:
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import { Link } from 'react-router'
import {
Segment as UISegment,
} from 'semantic-ui-react'
import NavMenu from '../components/NavMenu'
class MenuBar extends Component {
static propTypes = {
path: PropTypes.string.isRequired
}
render() {
const { path, } = this.props
return (
<UISegment>
<NavMenu activePath={path} />
</UISegment>
)
}
}
const mapStateToProps = (state, ownProps) => {
return {
path: ownProps.route ? ownProps.route.path : "/"
}
}
export default connect(mapStateToProps)(MenuBar)
Inside the NavMenu component, a semantic-ui menu component will compare activePath with its own path and highlight the active button.
Everything seems to work in theory; when I click on the different parts of the menu, a ##router/LOCATION_CHANGE action is emitted. In the redux dev tools, I see the state changing. However, mapStateToProps is never called and this component is never re-rendered.
Any ideas? I thought about using the react methods like shouldComponentUpdate, but it seems that react doesn't even realize the state or props are changing.
First thing to note is that you are not actually accessing router state from the store. If you look at the react-router-redux docs, it actually warns against doing so
You should not read the location state directly from the Redux store. This is because React Router operates asynchronously (to handle things such as dynamically-loaded components) and your component tree may not yet be updated in sync with your Redux state. You should rely on the props passed by React Router, as they are only updated after it has processed all asynchronous code.
Your container is reading data from ownProps, which is just the props that are passed into that container component. The example in the react-router-redux docs that you are referencing only works for a top-level route component (a component that is passed as the component prop to a React Router Route component). React Router passes the router data into all route components.
In your case, MenuBar is a child of whatever your top level route component is. Your two options are to
Pass the data you want into MenuBar down from your route component.
Use React Router's withRouter higher order component to inject the values into MenuBar https://github.com/ReactTraining/react-router/blob/v3/docs/API.md#withroutercomponent-options
Also, I believe the value you are looking for is ownProps.location.pathname rather than ownProps.route.path
Some code for option 1, since I'm assuming MenuBar isn't nested too deeply in your component tree:
If your route config is
<Router history={browserHistory}>
<Route path="/" component={AppLayout}>
<Route path="about" component={About}/>
<Route path="users" component={Users}/>
<Route path="*" component={NoMatch}/>
</Route>
</Router>
your AppLayout would be something like
const AppLayout = ({ children, location }) => {
return (
<div>
<MenuBar path={ location.pathname } />
{ children }
</div>
)
}
and MenuBar would receive the data your are looking for.
Related
When I go to one functional component using react-router, it renders twice.
However, when I refresh the page of that component, it only renders once.
For the test, created empty functional component like that:
import React from 'react'
const TestFunctional: React.FC<any> = () => {
console.log('Test===>>>') // console log twice when navigate to this component
return <></>
}
export default TestFunctional
Here is Router in App.tsx
import React from 'react'
import { Route, Switch, useLocation, withRouter } from 'react-router-dom'
import TestFunctional from 'views/Test'
const AnimatedSwitch = withRouter(({ location }) => (
<Switch>
<Route exact path="/" component={StartPage} />
<Route exact path="/test" component={TestFunctional} />
</Switch>
))
const App = () => {
return (
<div className="app">
<Web3ReactManager>
<AnimatedSwitch />
</Web3ReactManager>
</div>
)
}
export default App
I did not use React.StrictMode in index.tsx.
ReactDOM.render(
<ApolloProvider client={client}>
<Provider store={store}>
<ConnectedRouter history={history}>
<Web3ReactProvider getLibrary={getLibrary}>
<Web3ProviderNetwork getLibrary={getLibrary}>
<App />
</Web3ProviderNetwork>
</Web3ReactProvider>
</ConnectedRouter>
</Provider>
</ApolloProvider>,
document.getElementById('root')
)
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers
serviceWorker.unregister()
So it is some weird.
When I refresh this page, console.log('Test===>>>') show only once.
What is a mistake and how to fix the double render problem?
Why is that a problem? You should design/write your components assuming that it could re-render at anytime. React is even working on a new rendering mode where your component might be rendered multiple times before it actually gets "rendered in DOM".
As for why it actually renders twice? Not sure, might just be a quick of ReactDOM. As a side note, the documentation for component does have this warning for Route though:
When you use component (instead of render or children, below) the
router uses React.createElement to create a new React element from the
given component. That means if you provide an inline function to the
component prop, you would create a new component every render. This
results in the existing component unmounting and the new component
mounting instead of just updating the existing component. When using
an inline function for inline rendering, use the render or the
children prop (below).
While that shouldn't apply in this case, still handy to know.
I have a button in one component and on click I want to go to other component and pass to this component the state.
It's something like this:
import React, { Component } from 'react';
import { BrowserRouter, Route } from 'react-router-dom';
import AddNewQuestionPage from 'AddNewQuestionPage';
class AddQuestions extends Component {
state = {
questions: []
};
routeChange = () => {
let path = `/admin/add-new-question`;
this.props.history.push(path);
}
render() {
return (
<>
<button onClick={this.routeChange}>
Add new question
</button>
<BrowserRouter>
<Route exact={true} path='/admin/add-new-question' component={AddNewQuestionPage}/>
</BrowserRouter>
</>
)
}
}
And it doesn't work. On click I go to add-new-question url but the component AddNewQuestionPage doesn't render. It works if I put Route not in AddQuestions component, but in App component. It's the main component of the whole app and using Switch, there are set also other routes.
However I don't know how I can pass the state questions to AddNewQuestionPage component if it's rendered from App component? I can't just do:
<Route path='/admin/add-new-question' render={(props) => <AddNewQuestionPage {...props} questions={questions} />
because it doesn't know what is "questions". Lifting the state up to the main component doesn't seem a good solution for me. I was searching and I can't find how to do it...
You should use the this keyword on the question you're passing to the component.
So something like this
<Route path='/admin/add-new-question' render={(props) => <AddNewQuestionPage {...props} questions={this.state.questions} />
The best way to use react-router dom is :
Always make as a parent component for all.
The best way to change the path by clicking on a sort of component (button ...) is You label
Please check this guide out : https://reacttraining.com/react-router/web/guides/quick-start
I'm using React Router to create a multi page app. My main component is <App/> and it renders all of the routing to to child components. I'm trying to pass props via the route, and based on some research I did, the most common way for child components to tap into props passed down is via the this.props.route object that they inherit. However, this object is undefined for me. On my render() function in the child component, I console.log(this.props) and am return an object that looks like this
{match: Object, location: Object, history: Object, staticContext: undefined}
Doesn't look like the props I expected at all. Here is my code in detail.
Parent Component (I'm trying to pass the word "hi" down as a prop called "test" in all of my child components):
import { BrowserRouter as Router, HashRouter, Route, Switch } from 'react-router-dom';
import Link from 'react-router';
import React from 'react';
import Home from './Home.jsx';
import Nav from './Nav.jsx';
import Progress from './Progress.jsx';
import Test from './Test.jsx';
export default class App extends React.Component {
constructor() {
super();
this._fetchPuzzle = this._fetchPuzzle.bind(this);
}
render() {
return (
<Router>
<div>
<Nav />
<Switch>
<Route path="/" exact test="hi" component={Home} />
<Route path="/progress" test="hi" component={Progress} />
<Route path="/test" test="hi" component={Test} />
<Route render={() => <p>Page not found!</p>} />
</Switch>
</div>
</Router>
);
}
}
Child:
import React from 'react';
const CodeMirror = require('react-codemirror');
import { Link } from 'react-router-dom';
require('codemirror/mode/javascript/javascript')
require('codemirror/mode/xml/xml');
require('codemirror/mode/markdown/markdown');
export default class Home extends React.Component {
constructor(props) {
super(props);
console.log(props)
}
render() {
const options = {
lineNumbers: true,
theme: 'abcdef'
// mode: this.state.mode
};
console.log(this.props)
return (
<div>
<h1>First page bro</h1>
<CodeMirror value='code lol' onChange={()=>'do something'} options={options} />
</div>);
}
}
I'm pretty new to React so my apologies if I'm missing something obvious.
Thanks!
You can pass props to the component by making use of the render prop to the Route and thus inlining your component definition. According to the DOCS:
This allows for convenient inline rendering and wrapping without the
undesired remounting explained above.Instead of having a new React
element created for you using the component prop, you can pass in a
function to be called when the location matches. The render prop
receives all the same route props as the component render prop
So you can pass the prop to component like
<Route path="/" exact render={(props) => (<Home test="hi" {...props}/>)} />
and then you can access it like
this.props.test
in your Home component
P.S. Also make sure that you are passing {...props} so that the
default router props like location, history, match etc are also getting passed on to the Home component
otherwise the only prop that is getting passed down to it is test.
I have a strange behavior. Once I add the socket connection to the react / redux system, my main component always will be rerendered, as soon as a next Action is dispatched.
I also have the behavior, when I click again on a navigation link (dispatch the same routing action),
the compoenent also rerenders, even I stay on the same page.
Could anyone help me here to get on track?
Thanks a lot!
Setup
react 0.15.x
redux
react-router v4
react-router-redux
app.jsx and App Structure for the Container:
class Root extends React.Component {
render() {
return (
<Provider store={store}>
<Router history={history}>
<Route path="/" component={RootContainer}>
<IndexRoute component={HomePage} />
<Route path="/start" component={StartPage} />
<Route path="*" component={NotFoundPage} />
</Route>
</Router>
</Provider>
);
}
}
RootContainer
class RootContainer extends React.Component {
...
componentWillMount() {
this.connectToSocket();
this.joinChannel();
}
componentWillUnmount() {
this.socket.disconnect();
}
...
connectToSocket() {
this.socket = new Socket('/socket');
this.socket.connect();
this.socket.onOpen(() => {
this.props.connectState(); // ACTION CALL
});
this.socket.onError((err) => {
this.props.disconnectState(err); // ACTION CALL
}
}
...
reducer
Something is changing the store state. It appears your RootContainer component is connected to Redux, so it is likely rerendering each time. There are two main approaches to optimizing this:
Only connect lower-level components to the state that they need. Redux's connect higher order component only rerenders when mapStateToProps produces different results. Doing this at a more granular level lower in the component tree reduces the amount of components that have to rerender on store updates.
Extend React.PureComponent for any children that aren't connected to Redux, and don't need to rerender each time their parent does. By default, when a top-level component rerenders, all of its children rerender too. PureComponent can prevent that.
This is only general advise, it's hard to say what exactly you should do without seeing more of your app's structure.
I'd like to display a title in <AppBar /> that is somehow passed in from the current route.
In React Router v4, how would <AppBar /> be able to get the current route passed into it's title prop?
<Router basename='/app'>
<main>
<Menu active={menu} close={this.closeMenu} />
<Overlay active={menu} onClick={this.closeMenu} />
<AppBar handleMenuIcon={this.handleMenuIcon} title='Test' />
<Route path='/customers' component={Customers} />
</main>
</Router>
Is there a way to pass a custom title from a custom prop on <Route />?
In the 5.1 release of react-router there is a hook called useLocation, which returns the current location object. This might useful any time you need to know the current URL.
import { useLocation } from 'react-router-dom'
function HeaderView() {
const location = useLocation();
console.log(location.pathname);
return <span>Path : {location.pathname}</span>
}
In react router 4 the current route is in -
this.props.location.pathname.
Just get this.props and verify.
If you still do not see location.pathname then you should use the decorator withRouter.
This might look something like this:
import {withRouter} from 'react-router-dom';
const SomeComponent = withRouter(props => <MyComponent {...props}/>);
class MyComponent extends React.Component {
SomeMethod () {
const {pathname} = this.props.location;
}
}
If you are using react's templates, where the end of your react file looks like this: export default SomeComponent you need to use the higher-order component (often referred to as an "HOC"), withRouter.
First, you'll need to import withRouter like so:
import {withRouter} from 'react-router-dom';
Next, you'll want to use withRouter. You can do this by change your component's export. It's likely you want to change from export default ComponentName to export default withRouter(ComponentName).
Then you can get the route (and other information) from props. Specifically, location, match, and history. The code to spit out the pathname would be:
console.log(this.props.location.pathname);
A good writeup with additional information is available here: https://reacttraining.com/react-router/core/guides/philosophy
There's a hook called useLocation in react-router v5, no need for HOC or other stuff, it's very succinctly and convenient.
import { useLocation } from 'react-router-dom';
const ExampleComponent: React.FC = () => {
const location = useLocation();
return (
<Router basename='/app'>
<main>
<AppBar handleMenuIcon={this.handleMenuIcon} title={location.pathname} />
</main>
</Router>
);
}
Here is a solution using history Read more
import { createBrowserHistory } from "history";
const history = createBrowserHistory()
inside Router
<Router>
{history.location.pathname}
</Router>
Has Con Posidielov said, the current route is present in this.props.location.pathname.
But if you want to match a more specific field like a key (or a name), you may use matchPath to find the original route reference.
import { matchPath } from `react-router`
const routes = [{
key: 'page1'
exact: true,
path: '/page1/:someparam/',
component: Page1,
},
{
exact: true,
key: 'page2',
path: '/page2',
component: Page2,
},
]
const currentRoute = routes.find(
route => matchPath(this.props.location.pathname, route)
)
console.log(`My current route key is : ${currentRoute.key}`)
I think the author's of React Router (v4) just added that withRouter HOC to appease certain users. However, I believe the better approach is to just use render prop and make a simple PropsRoute component that passes those props. This is easier to test as you it doesn't "connect" the component like withRouter does. Have a bunch of nested components wrapped in withRouter and it's not going to be fun. Another benefit is you can also use this pass through whatever props you want to the Route. Here's the simple example using render prop. (pretty much the exact example from their website https://reacttraining.com/react-router/web/api/Route/render-func)
(src/components/routes/props-route)
import React from 'react';
import { Route } from 'react-router';
export const PropsRoute = ({ component: Component, ...props }) => (
<Route
{ ...props }
render={ renderProps => (<Component { ...renderProps } { ...props } />) }
/>
);
export default PropsRoute;
usage: (notice to get the route params (match.params) you can just use this component and those will be passed for you)
import React from 'react';
import PropsRoute from 'src/components/routes/props-route';
export const someComponent = props => (<PropsRoute component={ Profile } />);
also notice that you could pass whatever extra props you want this way too
<PropsRoute isFetching={ isFetchingProfile } title="User Profile" component={ Profile } />
Add
import {withRouter} from 'react-router-dom';
Then change your component export
export default withRouter(ComponentName)
Then you can access the route directly within the component itself (without touching anything else in your project) using:
window.location.pathname
Tested March 2020 with: "version": "5.1.2"