React-router-dom params undefined in nested route after using link - react-router

I need a switch component to have access to route params. The switch is rendered in one of the routes but its also rendered outside of it. Is there a way to get the same params in the component rendered outside of the route? Thanks for the help in advance!

It's usually a good pattern to not directly pass params through the route and keep those simple with the view component. You can use useContext, and then have each component(route) plug into that state using the useContext hook in the component.
for example...
app.js
import { useState } from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import { Routes } from "./auth/routes.js";
import { GlobalContext } from './globals/GlobalContext.js';
const App = () => {
// variables
const [someState, setSomeState] = useState('hello world');
// render
return (
<div>
<GlobalContext.Provider value={{someState, setSomeState}}>
<Router children={Routes} basename={process.env.REACT_APP_PUBLIC_URL} />
</GlobalContext.Provider>
</div>
);
}
GlobalContext.js
import { createContext } from 'react';
export const GlobalContext = createContext("");
routes.js
import { Route, Switch } from "react-router-dom";
// views
import ViewOne from '../views/ViewOne.js';
import ViewTwo from '../views/ViewTwo.js';
// globals
import { frontendLinks } from '../globals/index.js';
export const Routes = (
<Switch>
<Route exact path={frontendLinks.viewOne} component={ViewOne}></Route>
<Route exact path={frontendLinks.viewTwo} component={ViewTwo}></Route>
</Switch>
);
now the views...
import { useContext } from 'react';
// globals
import { GlobalContext } from '../globals/GlobalContext.js';
const ViewOne = () => {
const { someState } = useContext(GlobalContext);
return (
<div>
<h1>{someState}<h1>
</div>
)
}
export default ViewOne;
and
import { useContext } from 'react';
// globals
import { GlobalContext } from '../globals/GlobalContext.js';
const ViewTwo = () => {
const { someState } = useContext(GlobalContext);
return (
<div>
<h1>{someState}<h1>
</div>
)
}
export default ViewTwo;
If you don't want to manage shared state in your app.js file, I suggest you check out this video for managing useContext state in different files > https://www.youtube.com/watch?v=52W__dKdNnU

Related

How to listen to query param change event in react-router v5

I'd like to be able to listen to query param change events, preferably via a hook, but anything would be nice. I can't find anything that suggests it's even possible with react-router, so other suggestions without it are welcome too.
There's nothing in react-router-dom#5 that directly does this, so you'd need to implement this yourself. You can use the useLocation hook to access the location.search value to create a URLSearchParams object, then a useEffect hook to issue the side-effect based on any specific queryString parameter updating.
Example:
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
...
const { search } = useLocation();
const searchParams = new URLSearchParams(search);
const param = searchParams.get("param");
useEffect(() => {
// issue side-effect
}, [param]);
For RRDv5 there is this recipe to abstract the access of the query params:
import { useMemo } from 'react';
import { useLocation } from 'react-router-dom';
const useQuery = () => {
const { search } = useLocation();
return useMemo(() => new URLSearchParams(search), [search]);
};
...
import { useEffect } from 'react';
import { useQuery } from '../path/to/hooks';
...
const searchParams = useQuery();
const param = searchParams.get("param");
useEffect(() => {
// issue side-effect
}, [param]);
You can use useQuery and the useEffect hook to create another custom hook.
import { useEffect } from 'react';
import { useQuery } from '.';
const useQueryParam = (paramKey, cb) => {
const searchParams = useQuery();
const param = searchParams.get(paramKey);
useEffect(() => {
if (param) {
cb(param);
}
}, [param]);
};
...
import { useQueryParam } from '../path/to/hooks';
...
useQueryParam(
"myParameter",
(paramValue) => {
// do something with "myParameter" param value
},
);
did you tried below
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function MyComponent() {
const location = useLocation();
useEffect(() => {
console.log('Location changed');
}, [location]);
...
}

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,
};

Testing method in react component don't work

I have simple component in React. I want to test method in this component when user click button. I have test for that but finally don't pass.
Component:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import axios from 'axios';
class TestInvokingMethod extends Component {
onClick() {
}
render() {
return (
<div>
<input id='buttonTest' type='button' value={10} onClick={this.onClick} />
</div>
);
}
}
export default TestInvokingMethod;
And test for that:
import React from 'react';
import { shallow } from 'enzyme';
import TestInvokingMethod from '../../components/TestComponent/TestInvokeMethod';
const component = shallow(
<TestInvokingMethod />
);
test('Testing invoke method', () => {
const mockFn = jest.fn();
component.instance().onClick = mockFn;
component.update();
component.find('#buttonTest').simulate('click');
expect(component.instance().onClick.mock.calls.length).toBe(1);
});
Try using Jest's SpyOn
const spy = expect.spyOn(wrapper.instance(), "onClick");
wrapper.update();
wrapper.find('#buttonTest').simulate('click');
expect(spy).toHaveBeenCalled();
In addition to Garry's answer. In scenarios where wrapper.update() does not work, try updating its instance forcefully using wrapper.instance().forceUpdate().

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() });
});
});

react-router-redux: Module build failed: SyntaxError: Unexpected token ...reducers

//app.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, combineReducers, applyMiddleware } from 'redux';
import createHistory from 'history/createBrowserHistory';
import { Route } from 'react-router';
import { ConnectedRouter, routerReducer, routerMiddleware, push } from 'react-router-redux';
import App from './components/app.js';
import reducers from './reducers';
const history = createHistory();
const middleware = routerMiddleware(history);
// Add the reducer to your store on the `router` key
// Also apply our middleware for navigating
const store = createStore({
...reducers,
router: routerReducer
},
applyMiddleware(middleware)
);
const About = () => {
return (<div>
Will this work?
</div>);
}
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
<div>
<Route exact path="/" component={App}/>
<Route path="/about" component={About}/>
</div>
</ConnectedRouter>
</Provider>
, document.querySelector('#app'));
Here is my reducers/index.js code.
//reducers/index.js
import { combineReducers } from 'redux';
import { mobileLinks } from './reducer_header';
import UserDetailsReducer from './reducer_user_details';
const rootReducer = combineReducers({
mobileLinks,
userDetails: UserDetailsReducer
});
export default rootReducer;
What do I do to fix this? I'm unable to find any examples for the new version of react-router-redux. I've tried moving the routerReducer to reducers/index.js but that didn't work either. Can someone please help?
Most likely you do not have Babel set to transpile the Object Spread Operator, you can read up on it here.
You can simply install the Babel "Object rest spread transform" preset like so:
npm install --save-dev babel-plugin-transform-object-rest-spread
And add it to your list of plugins:
{
"plugins": [
// Other plugins...
"transform-object-rest-spread"
]
}