How to access history.listen in a React component? - react-router

I have a specific component who would like to be notified every time the user navigates. Is there some way to access the history passed into the router?
<Router history={history}>
{// ...}
</Router>
Child component:
var Component = React.createClass({
componentDidMount: function() {
// history.listen(this.onRouteChange);
},
onRouteChange: function() {},
render: function() {...},
});

I've noticed that this works:
import { browserHistory } from 'react-router';
var Component = React.createClass({
componentDidMount: function() {
browserHistory.listen(this.onRouteChange);
},
...
});
But it seems like I'd want to use the actual history passed into the router rather than blindly using browserHistory. In some instances I pass in hashHistory instead. Would still appreciate a better solution!

Use withRouter from 'react-router' like this:
import React from 'react'
import PropTypes from 'prop-types'
import { withRouter } from 'react-router'
Following a simple component that shows the pathname of the current location. Works the same for history prop, just use history instead of location then.
class ShowTheLocation 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.
const ShowTheLocationWithRouter = withRouter(ShowTheLocation)
From: https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/withRouter.md

Related

react-router 6 Navigate to using params

In v5 i have such structure
{
path: '/someurl/:id',
exact: true,
render: ({ params }) => (<Redirect to={`/someurl/extraurl/${params.id}`} />),
}
How to refactor this to V6?
react-router-dom v6 no longer has route props, so you'll need to create a new component to gather the "props", or match.params in this case, and render the redirect as a Navigate component.
const MyRedirect = () => {
const { id } = useParams();
return <Navigate to={`/someurl/extraurl/${id}`} replace />;
};
...
{
path: '/someurl/:id',
element: <MyRedirect />,
}
...
<Route path={obj.path} element={obj.element} />
The accepted answer will work but I'll add my solution too, since it's a bit more dynamic. You can set up a function component that will make use of the useParams hook and the generatePath function so your intended destination gets the params from the initial route (whatever they may be):
import React, { FunctionComponent } from 'react';
import { generatePath, Navigate, useParams } from 'react-router-dom';
interface IProps {
to: string;
replace?: boolean;
state?: any;
}
const Redirect: FunctionComponent<IProps> = ({ to, replace, state }) => {
const params = useParams();
const redirectWithParams = generatePath(to, params);
return (
<Navigate to={redirectWithParams} replace={replace} state={state} />
);
};
export default Redirect;
Using this should work with your first example (and any other routes / redirects with dynamic params).

How to create a link that goes back in react-router-dom v4

I know I can access history.goBack() to go back in the router history.
However, I'd like to create a <Link /> tag that has this functionality and relies on the to property (href) to navigate back rather than an onClick.
Is this possible?
I may have a solution to your problem using the context api.
But I strongly believe that it would be easier to use history.goBack().
First you'll need to wrap the App component inside a router:
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
ReactDOM.render(
<Router>
<App />
</Router>,
document.getElementById('root'),
);
Then in your your App/index.js file you'll need to listen to the location change event and set your state accordingly:
import React, { Component } from 'react';
import { Switch, Route } from 'react-router-dom';
import { withRouter } from 'react-router'
class App extends Component {
state = { prevLocation: '' };
// Use the context api to retrieve the value in your Link
getChildContext = () => (
{
prevLocation: this.state.prevLocation,
}
);
componentWillReceiveProps(nextProps) {
if (nextProps.location !== this.props.location) {
this.setState({ prevLocation: this.props.location.pathname });
}
}
render() {
return (
<div>
<Switch>
// ...
</Switch>
</div>
);
}
}
App.childContextTypes = {
prevLocation: PropTypes.string,
};
export default withRouter(App);
Then in can create a GoBack component and use the context API to retrieve the value the previous path.
import React from 'react';
class GoBack extends React.Component {
render() {
return <Link to={this.context.prevLocation}>click</Link);
}
}
GoBack.contextTypes = {
prevLocation: PropTypes.string,
};

React Native, Navigating a prop from one component to another

handleShowMatchFacts = id => {
// console.log('match', id)
return fetch(`http://api.football-api.com/2.0/matches/${id}?Authorization=565ec012251f932ea4000001fa542ae9d994470e73fdb314a8a56d76`)
.then(res => {
// console.log('match facts', matchFacts)
this.props.navigator.push({
title: 'Match',
component: MatchPage,
passProps: {matchInfo: res}
})
// console.log(res)
})
}
I have this function above, that i want to send matchInfo to matchPage.
I take in that prop as follows below.
'use strict'
import React from 'react'
import { StyleSheet, View, Component, Text, TabBarIOS } from 'react-native'
import Welcome from './welcome.js'
import More from './more.js'
export default class MatchPage extends React.Component {
constructor(props) {
super(props);
}
componentWillMount(){
console.log('mathc facts ' + this.props.matchInfo._bodyInit)
}
render(){
return (
<View>
</View>
)
}
}
All the info I need is in that object - 'this.props.matchInfo._bodyInit'. My problem is that after '._bodyInt', I'm not sure what to put after that. I've tried .id, .venue, and .events, they all console logged as undefined...
You never change props directly in React. You must always change the state via setState and pass state to components as props. This allows React to manage state for you rather than calling things manually.
In the result of your api call, set the component state:
this.setState({
title: 'Match',
component: MatchPage,
matchInfo: res
}
Then pass the state as needed into child components.
render() {
return(
<FooComponent title={this.state.title} matchInfo={this.state.matchInfo} />
);
}
These can then be referenced in the child component as props:
class FooComponent extends Component {
constructor(props) {
super(props);
}
componentWillMount() {
console.log(this.props.title);
console.log(this.props.matchInfo);
// Etc.
}
}
If you need to reference these values inside the component itself, reference state rather than props.
this.state.title;
this.state.matchInfo;
Remember components manage their own state and pass that state as props to children as needed.
assuming you are receiving json object as response , you would need to parse the response before fetching the values.
var resp = JSON.parse(matchInfo);
body = resp['_bodyInit'];

Using Jest to test a Link from react-router v4

I'm using jest to test a component with a <Link> from react-router v4.
I get a warning that <Link /> requires the context from a react-router <Router /> component.
How can I mock or provide a router context in my test? (Basically how do I resolve this warning?)
Link.test.js
import React from 'react';
import renderer from 'react-test-renderer';
import { Link } from 'react-router-dom';
test('Link matches snapshot', () => {
const component = renderer.create(
<Link to="#" />
);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
The warning when the test is run:
Warning: Failed context type: The context `router` is marked
as required in `Link`, but its value is `undefined`.
You can wrap your component in the test with the StaticRouter to get the router context into your component:
import React from 'react';
import renderer from 'react-test-renderer';
import { Link } from 'react-router-dom';
import { StaticRouter } from 'react-router'
test('Link matches snapshot', () => {
const component = renderer.create(
<StaticRouter location="someLocation" context={context}>
<Link to="#" />
</StaticRouter>
);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
Have a look at the react router docs about testing
I had the same issue and using StaticRouter would still require the context which needed more configuration to have it available in my test, so I ended up using the MemoryRouter which worked very well and without any issues.
import React from 'react';
import renderer from 'react-test-renderer';
import { MemoryRouter } from 'react-router-dom';
// SampleComponent imports Link internally
import SampleComponent from '../SampleComponent';
describe('SampleComponent', () => {
test('should render', () => {
const component = renderer
.create(
<MemoryRouter>
<SampleComponent />
</MemoryRouter>
)
.toJSON();
expect(component).toMatchSnapshot();
});
});
The answer of #Mahdi worked for me! In 2023 if you want to test a component that includes <Link> or <NavLink>, we just need to wrap it with the <MemoryRouter> in the test file:
// App.test.js
import { render, screen } from "#testing-library/react";
import MyComponent from "./components/MyComponent";
import { MemoryRouter } from "react-router-dom"; // <-- Import MemoryRouter
test("My test description", () => {
render(
<MemoryRouter> // <-- Wrap!
<MyComponent />
</MemoryRouter>
);
});
my test like this:
import * as React from 'react'
import DataBaseAccout from '../database-account/database-account.component'
import { mount } from 'enzyme'
import { expect } from 'chai'
import { createStore } from 'redux'
import reducers from '../../../reducer/reducer'
import { MemoryRouter } from 'react-router'
let store = createStore(reducers)
describe('mount database-account', () => {
let wrapper
beforeEach(() => {
wrapper = mount(
< MemoryRouter >
<DataBaseAccout store={store} />
</MemoryRouter >
)
})
afterEach(() => {
wrapper.unmount()
wrapper = null
})
})
but I don't konw why MemoryRouter can solve this。
Above solutions have a common default defact:
Can't access your component's instance! Because the MemoryRouter or StaticRouter component wrapped your component.
So the best to solve this problem is mock a router context, code as follows:
import { configure, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
describe('YourComponent', () => {
test('test component with react router', () => {
// mock react-router context to avoid violation error
const context = {
childContextTypes: {
router: () => void 0,
},
context: {
router: {
history: createMemoryHistory(),
route: {
location: {
hash: '',
pathname: '',
search: '',
state: '',
},
match: { params: {}, isExact: false, path: '', url: '' },
}
}
}
};
// mount component with router context and get component's instance
const wrapper = mount(<YourComponent/>, context);
// access your component as you wish
console.log(wrapper.props(), wrapper.state())
});
beforeAll(() => {
configure({ adapter: new Adapter() });
});
});

Is there an ES6 syntax for React-Router HOC/withRouter?

Is there a way to use the ES6 extend feature with the React-Router "withRouter" component?
Something like this:
import { withRouter } from 'react-router';
export default class extends withRouter {
...
//Use react router history prop to navigate back a page.
handleSomeEvent() {
this.props.router.goBack();
}
...
}
Or am I stuck using the old composition pattern?
var MyComponent = React.createClass({
...
});
export default withRouter(MyComponent);
Yes, it's easy, see bellow (not saying you should redirect 2s after component was mounted... just an example).
BTW my version of react-router is 2.6 (2.4+ required for withRouter)
import React, { Component } from 'react';
import { withRouter } from 'react-router';
class MyComponent extends Component {
componentDidMount() {
setTimeout(() => {
this.props.router.push('my-url')
}, 2000);
}
render() {
return (
<div>My Component</div>
);
}
}
export default withRouter(MyComponent);
You could use it like this.
#withRouter
export default class extends withRouter {
...
//Use react router history prop to navigate back a page.
handleSomeEvent() {
this.props.router.goBack();
}
...
}
But you would have to include babel-plugin-transform-decorators-legacy