How to access match outside of Route? - react-router

What is the best way to accomplish what I am trying in the code below? App cannot access match.params for routes defined inside it, but I would like to pass parts of the state to child components based on the url params. I cannot use hooks like useRouteMatch() because App is a stateful class component. I think I can do this with the the Route render method, but it looks like React Router docs say that method is deprecated.
So is there a design pattern similar to this that lets me keep all route logic in App and just pass props to child components based on params, that doesnt use the render method of Route?
class App extends React.Component {
state = { things: this.props.things };
render() {
return (
<Switch>
<Route path='/thing/:thingId'>
<OneThing thing={this.state.things.find(thing => thing.id === match.params.thingId)} />
</Route>
<Route path='/things/:thingTag'>
<MultipleThings things={this.state.things.filter(thing => thing.tag === match.params.thingTag)} />
</Route>
</Switch>
);
}
}

with <Route render>
<Route path='/thing/:thingId'
render={(route) => <OneThing thing={route.match.params.thingId} />} />
with <Route children> ver 5.1
<Route
path='/thing/:thingId'
children={({ match }) => (
<OneThing thing={match.params.thingId} />
)}
/>

try using withRouter
import React from "react";
import PropTypes from "prop-types";
import { withRouter } from "react-router";
// A simple component that shows the pathname of the current location
class OneThing extends React.Component {
static propTypes = {
match: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
};
render() {
const { match, location, history } = this.props;
return <div>You are now at {location.pathname}</div>;
}
}
// Create a new component that is "connected" (to borrow redux
// terminology) to the router.
export default withRouter(ShowTheLocation);

Related

Props object is empty with history.push

I have an onClick handler used for filtering data that routes back to the same component, but with a different url. However, when this same components renders again, I cannot access props.location.
Lots of code has been left out for brevity.
Component:
import { useHistory } from 'react-router-dom';
const Dashboard = (props) => {
const history = useHistory();
console.log(props) // Empty
useEffect(() => {
console.log(props) // Empty
})
const handleFilter = argument => {
history.push('/filter'); // 'argument' left out to test routing, and to ensure props.location is accessible
}
return (
<button onClick={() => handleFilter('someArgumentHere')}>Filter</button>
)
}
Router:
import React from 'react';
import {
BrowserRouter as Router,
Switch,
Route,
Redirect,
} from 'react-router-dom';
const PrivateRoute = ({ exact, path, component: Component }) => {
return (
<Route
exact={exact}
path={path}
render={props => (
<div>
<Navbar />
<Component {...props} />
</div>
)}
/>
);
};
<Router>
<Switch>
<PrivateRoute exact component={Dashboard} path="/" />
<PrivateRoute exact component={Dashboard} path="/filteredPriority" />
</Switch>
</Router>
When I click on handleFilter, the routing works. Meaning, my browser navigates from '/' to '/filteredPriority', and I see the same content, which is desired. But, since this is a filter, I want to access the url params via props.location, and it's empty. I don't know why.
Figured it out. Needed to wrap my Dashboard component in withRouter.

MemoryRouter and jest test

https://reacttraining.com/react-router/web/guides/testing
The react-router testing documentation is bit obscure to me.
How to write a test to check a route is rendered
A Component. - APage.js
import React, { Component } from 'react'
export default class APage extends Component {
render() {
return (
<div>
A Page
</div>
)
}
}
Writing a unit test to check , as per documentation.
routes.test.js
import React from 'react'
import { render } from "react-dom";
import APage from './APage'
import {MemoryRouter} from 'react-router-dom';
test("render route", () => {
render(
<MemoryRouter initialEntries={["/apage"]}>
<APage />
</MemoryRouter>
);
});
It gives an error,
Invariant Violation: Target container is not a DOM element.
for render.
How do I write a basic test, like to test a component is rendered on a route.
I'd like to comment on Remi's solution, since the API in React Router v6 is a little different (and the link to the docs leads now to a 404):
import { Router } from 'react-router-dom';
test("render route", () => {
const history = createMemoryHistory();
render(
<Router
location={history.location} // history.location has a default value of '/'
navigator={history}
>
<APage />
</Router>
);
})
see github repo here
I think you should use Router instead. Since that uses BrowserRouter. (see alternatives section on react router example page)
import { Router } from 'react-router-dom';
test("render route", () => {
const history = createMemoryHistory();
history.push('/apage');
render(
<Router history={history}>
<APage />
</Router>
);
});
It could be that you should also add your page in a Route, but I'm not sure.
Then it would be something like:
import { Router, Route } from 'react-router-dom';
test("render route", () => {
const history = createMemoryHistory();
history.push('/apage');
render(
<Router history={history}>
<Route path='/aroute' render={(props) => (<APage {...props} />)} />
</Router>
);
});
Ok. Route testing has to be done by enzyme. not just using jest.
Followed https://medium.com/#antonybudianto/react-router-testing-with-jest-and-enzyme-17294fefd303
Used enzyme mount to test.

Prefetch based on route with React Loadable and React Router?

Im using React Router and React Loadable to code split my app based on route:
In App.js:
<Router>
<Switch>
<Route exact path="/page1" component={Component1Loader} />
<Route exact path="/page2" component={Component2Loader} />
In Component1Loader:
import Loadable from 'react-loadable';
const LoadableComponent = Loadable({
loader: () => import('./ActualComponent1'),
loading: Loading,
});
export default class Component1Loader extends React.Component {
render() {
return <LoadableComponent {...this.props} />;
}
}
In Component2Loader:
import Loadable from 'react-loadable';
const LoadableComponent = Loadable({
loader: () => import('./ActualComponent2'),
loading: Loading,
});
export default class Component2Loader extends React.Component {
render() {
return <LoadableComponent {...this.props} />;
}
}
How can I preload components based on route? Eg from the front page most users will then navigate to /page1 so I would like to preload this.
When on /page1 I would then like to prefetch /page2.
The component created by Loadable exposes a static preload method. In your case you need to export each LoadableComponent and preload in appropriate file *.js you want to. Example:
import { LoadableComponent } from './Component1Loader'
// Preload here
LoadableComponent.preload()
...
<Router>
<Switch>
<Route exact path="/page1" component={Component1Loader} />
<Route exact path="/page2" component={Component2Loader} />
https://github.com/jamiebuilds/react-loadable#preloading

Migrating from React Router v2 to v4

So, I'm currently using react-router v2 as follows:
import { IndexRoute, Router, Route, Redirect } from 'react-router';
import App from './components/App';
....
render () {
return (
<ApolloProvider store={store} client={client}>
<Router history={history}>
<Route path="/" component={App}>
<IndexRoute component={PhotoGrid} />
<Route path="/view/:postId" component={Single}></Route>
<Route path="/login" component={LoginUser}></Route>
</Route>
</Router>
</ApolloProvider>
)
}
}
export default MainApp;
App.js
....
import Main from './Main';
const allPostsCommentsQuery = graphql(All_Posts_Comments_Query, {
options: {
cachePolicy: 'offline-critical',
fetchPolicy: 'cache-first',
},
});
function mapStateToProps(state) {
return {
auth: state.auth
};
}
export const mapDispatchToProps = (dispatch) => {
return bindActionCreators(actionCreators, dispatch);
}
export default compose(
allPostsCommentsQuery,
connect(mapStateToProps, mapDispatchToProps)
)(Main);
Main.js
class Main extends React.Component {
constructor (props) {
super(props);
}
componentWillMount () {
if (!this.props.auth.token){
this.context.router.push('/login');
}
}
render () {
return (
<div>
<h1>
<Link to="/">Flamingo City</Link>
</h1>
{ React.cloneElement(this.props.children, this.props) }
</div>
);
}
}
Main.contextTypes = {
router: function() { React.PropTypes.func.isRequired }
};
export default Main;
How do I convert my current v2 router to v4? What I am not clear on, is the parent nested element:
<Route path="/" component={App}>
In all the v2 -> v4 conversion examples I have seen thus far, none clearly explain what happens to the child elements. Am I expected to place the child elements within the App.js component itself, and if so, in the version of my App.js, how would that work as the first sign of any navigation actually occurs with Main.js?
Really useful post on github where you can see all the important parts of migrating to v4.
https://gist.github.com/kennetpostigo/7eb7f30162253f995cd4371d85c1e196
Also explaining how to go about child routes. Basically, you are supposed to place a Match inside App.js so this parent component will become responsible for its own part of child routes, an so on with every parent component.
Haven't tried this, let me know how it goes!

React router throws and error when getting async component

I'm using react, redux and react router amoung others to build and example app.
I'm trying to load asynchronously different parts of my application. I've divided my app in ducks and I'm following this example https://github.com/insin/react-examples/tree/master/code-splitting-redux-reducers
But I'm getting this error:
Uncaught Invariant Violation: The root route must render a single element
When trying to get async component with getComponent method of react router.
I'm using:
react-router 2.0.1
My routes:
export default function configureRoutes(reducerRegistry) {
return(
<Route>
<Route component={Landing}>
<Route path='/login' component={Login}/>
<Route path='/register' component={Register}/>
</Route>
<Route path="admin" getComponent={(location, cb) => {
require.ensure([], require => {
cb(null, require('./containers/admin'))
})
}}/>
<Route component={App}>
<Route path='/' component={Home} />
</Route>
</Route>
)}
My component
class Admin extends Component {
componentDidMount() {
this.props.load()
}
render() {
const { message, isFetching } = this.props
return (
<div>
<p>{message}</p>
<p>This module was loaded via chunk </p>
{loading && <p>Doing some fake loading ...</p>}
</div>
)
}
}
Admin.propTypes = {
message: PropTypes.string.isRequired,
isFetching: PropTypes.bool.isRequired,
load: PropTypes.string.isRequired
}
const mapStateToProps = state => state.admin
function mapDispatchToProps(dispatch) {
return bindActionCreators({ load }, dispatch)
}
export default connect(mapStateToProps, mapDispatchToProps)(Admin)
Does anyone have the same error? any ideas? Anyone have something similar working?
Thanks community!
Update: Added index.js for clarity
import configureRoutes from './routes'
import configureStore from './store/configureStore'
import coreReducers from './modules/core'
import ReducerRegistry from './reducer-registry'
var reducerRegistry = new ReducerRegistry(coreReducers)
// Configure hot module replacement for core reducers
if (process.env.NODE_ENV !== 'production') {
if (module.hot) {
module.hot.accept('./modules/core', () => {
var nextCoreReducers = require('./modules/core')
reducerRegistry.register(nextCoreReducers)
})
}
}
const routes = configureRoutes(reducerRegistry)
const store = configureStore(reducerRegistry)
render(
<I18nextProvider i18n={i18n}>
<Provider store={store}>
<Router history={browserHistory} routes={routes} />
</Provider>
</I18nextProvider>,
document.getElementById('root')
)
I think your root <Route> is missing the component field.
You need to specify either component or getComponent for every parent route, as this will be the component that the current child route’s component gets passed to as this.props.children.
Rather than
export default function configureRoutes(reducerRegistry) {
return (
<Route>
you want something like
export default function configureRoutes(reducerRegistry) {
return (
<Route component={App}>
In this case, you probably won’t need another App route below.