React: pass a query string into a path - reactjs

I am trying to apply the answer given in this post
How to parse a query string in React Router
but it's not working.
This is my url:
`/api/lexemes?searchTerm=${searchTerm}&optionValue=${optionValue}`
First, nothing shows when I run this code:
let search = window.location.search;
let params = new URLSearchParams(search);
let foo = params.get('searchTerm');
console.log(foo);
nor does this one:
let url = this.props.location.search;
let params = queryString.parse(url);
console.log(params);
When I do:
console.log(this.props);
The console shows Location with search= ""
Please note that I am using Class Components, does it work only with functional ones? If you could tell me how to do this with a class component that would be great.
My paths in App.js look like this now:
<Switch>
<Route exact path="/" component={Main} />
...
</Switch>
What I would like to have is:
<Route exact path="/:search/:option" component={Main} />
This is my axios call in Main.js
handleSubmit = (searchTerm, optionValue) => {
axios
.get(`/api/lexemes?searchTerm=${searchTerm}&optionValue=${optionValue}`, {
})
.then(
(res) => {
if (Array.isArray(res.data) && res.data.length === 0) {
this.setState({
noData: 1,
});
} else {
this.setState({
words: res.data,
currentScreen: "WordItem",
});
}
},
(error) => {
alert("handleSubmit error " + error);
}
);
};

In this case, if you want to use
<Route exact path="/:search/:option" component={Main} />
This is the code for App.js
import React from "react";
import "./styles.css";
import { Switch, Route, BrowserRouter } from "react-router-dom";
import { Main } from "./Main";
export default function App() {
return (
<BrowserRouter>
<Switch>
<Route path="/:search/:option" component={Main} />
</Switch>
</BrowserRouter>
);
}
This is the code for component Main:
import React, { Component } from "react";
class Main extends Component {
render() {
console.log("location match params", this.props.match.params);
return <div>Home</div>;
}
}
export { Main };
This is class component and you will have access to params from Main component.
Instead of
let url = this.props.location.search;
let params = queryString.parse(url);
console.log(params);
Use this.props.match.location to access params from <Route exact path="/:search/:option" component={Main} />

Related

Testing navigation in React component

I'd like to test that the url changes, when a submit button is pressed. As part of the test, I'm checking that the initial url is "/auth" and the url becomes "/".
A simpler test is failing, though, with the initial url test.
Test:
it("displays an authcode and submit button", async() => {
history = createMemoryHistory();
const root = document.createElement('div');
document.body.appendChild(root);
render(
<MemoryRouter initialEntries={["/auth"]}>
<App />
</MemoryRouter>,
root
);
expect(screen.queryByTestId('bad-code-message').classList.contains('hidden')).toBe(true);
expect(screen.getByLabelText('Auth code:')).toBeVisible();
expect(screen.getByRole('button')).toBeVisible();
expect(location.pathname).toBe("/auth");
});
App component:
import React from "react";
import { Route } from "react-router-dom";
import { ProtectedRoute } from './ProtectedRoute';
import { CreateProfileWithRouter } from './CreateProfileComponent';
import { ActivityList } from './ActivityListComponent';
import { TokenEntryWithRouter } from './TokenEntryComponent';
export class App extends React.Component {
render() {
return (
<div>
<ProtectedRoute exact path="/" component={ActivityList} />
<Route path="/login" component={CreateProfileWithRouter} />
<Route path="/auth" component={TokenEntryWithRouter} />
</div>
);
}
}
Result:
expect(received).toBe(expected) // Object.is equality
Expected: "/auth"
Received: "/"
After some more trial and error, I figured something out. "/" is the initial url, but I don't know how to change that. I'm passing the url that the component will navigate to and asserting that "/" is the url, at the beginning, and, when navigation is tested, I assert the url has changed to the passed in url.
I'm also using Router instead of MemoryRouter. I had a hunch from the docs that the history prop, which is passed into the component (with "withRouter"), gets changed in a way that could be tested.
Before all tests:
beforeEach(() => {
jest.resetAllMocks();
createPermanentAuthSpy = jest.spyOn(yasClient, "createPermanentAuth");
history = createMemoryHistory();
const root = document.createElement('div');
document.body.appendChild(root);
render(
<Router history={history}>
<TokenEntryWithRouter navigateToOnAuthentication="/dummy" />
</Router>,
root
);
token = screen.getByLabelText('Auth code:');
expect(screen.queryByTestId('bad-code-message').classList.contains('hidden')).toBe(true);
expect(history.location.pathname).toBe("/");
});
Testing navigation:
it("navigates to '/', when a good token is entered.", async() => {
createPermanentAuthSpy.mockImplementationOnce(() => Promise.resolve(true));
await act(async() => {
fireEvent.change(token, { target: { value: '1' } });
fireEvent.submit(screen.getByTestId('create-permanent-auth'));
});
expect(createPermanentAuthSpy).toHaveBeenCalledTimes(1);
expect(token.classList.contains('valid-data')).toBe(true);
expect(screen.queryByTestId('bad-code-message').classList.contains('hidden')).toBe(true);
expect(history.location.pathname).toBe("/dummy");
});

Detect Route Change with react-router

I have to implement some business logic depending on browsing history.
What I want to do is something like this:
reactRouter.onUrlChange(url => {
this.history.push(url);
});
Is there any way to receive a callback from react-router when the URL gets updated?
You can make use of history.listen() function when trying to detect the route change. Considering you are using react-router v4, wrap your component with withRouter HOC to get access to the history prop.
history.listen() returns an unlisten function. You'd use this to unregister from listening.
You can configure your routes like
index.js
ReactDOM.render(
<BrowserRouter>
<AppContainer>
<Route exact path="/" Component={...} />
<Route exact path="/Home" Component={...} />
</AppContainer>
</BrowserRouter>,
document.getElementById('root')
);
and then in AppContainer.js
class App extends Component {
componentWillMount() {
this.unlisten = this.props.history.listen((location, action) => {
console.log("on route change");
});
}
componentWillUnmount() {
this.unlisten();
}
render() {
return (
<div>{this.props.children}</div>
);
}
}
export default withRouter(App);
From the history docs:
You can listen for changes to the current location using
history.listen:
history.listen((location, action) => {
console.log(`The current URL is ${location.pathname}${location.search}${location.hash}`)
console.log(`The last navigation action was ${action}`)
})
The location object implements a subset of the window.location
interface, including:
**location.pathname** - The path of the URL
**location.search** - The URL query string
**location.hash** - The URL hash fragment
Locations may also have the following properties:
location.state - Some extra state for this location that does not reside in the URL (supported in createBrowserHistory and
createMemoryHistory)
location.key - A unique string representing this location (supported
in createBrowserHistory and createMemoryHistory)
The action is one of PUSH, REPLACE, or POP depending on how the user
got to the current URL.
When you are using react-router v3 you can make use of history.listen() from history package as mentioned above or you can also make use browserHistory.listen()
You can configure and use your routes like
import {browserHistory} from 'react-router';
class App extends React.Component {
componentDidMount() {
this.unlisten = browserHistory.listen( location => {
console.log('route changes');
});
}
componentWillUnmount() {
this.unlisten();
}
render() {
return (
<Route path="/" onChange={yourHandler} component={AppContainer}>
<IndexRoute component={StaticContainer} />
<Route path="/a" component={ContainerA} />
<Route path="/b" component={ContainerB} />
</Route>
)
}
}
Update for React Router 5.1+.
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function SomeComponent() {
const location = useLocation();
useEffect(() => {
console.log('Location changed');
}, [location]);
...
}
react-router v6
In react-router v6, this can be done by combining the useLocation and useEffect hooks
import { useLocation } from 'react-router-dom';
const MyComponent = () => {
const location = useLocation()
React.useEffect(() => {
// runs on location, i.e. route, change
console.log('handle route change here', location)
}, [location])
...
}
For convenient reuse, you can do this in a custom useLocationChange hook
// runs action(location) on location, i.e. route, change
const useLocationChange = (action) => {
const location = useLocation()
React.useEffect(() => { action(location) }, [location])
}
const MyComponent1 = () => {
useLocationChange((location) => {
console.log('handle route change here', location)
})
...
}
const MyComponent2 = () => {
useLocationChange((location) => {
console.log('and also here', location)
})
...
}
If you also need to see the previous route on change, you can combine with a usePrevious hook
const usePrevious = (value) => {
const ref = React.useRef()
React.useEffect(() => { ref.current = value })
return ref.current
}
const useLocationChange = (action) => {
const location = useLocation()
const prevLocation = usePrevious(location)
React.useEffect(() => {
action(location, prevLocation)
}, [location])
}
const MyComponent1 = () => {
useLocationChange((location, prevLocation) => {
console.log('changed from', prevLocation, 'to', location)
})
...
}
It's important to note that all the above fire on the first client route being mounted, as well as subsequent changes. If that's a problem, use the latter example and check that a prevLocation exists before doing anything.
If you want to listen to the history object globally, you'll have to create it yourself and pass it to the Router. Then you can listen to it with its listen() method:
// Use Router from react-router, not BrowserRouter.
import { Router } from 'react-router';
// Create history object.
import createHistory from 'history/createBrowserHistory';
const history = createHistory();
// Listen to history changes.
// You can unlisten by calling the constant (`unlisten()`).
const unlisten = history.listen((location, action) => {
console.log(action, location.pathname, location.state);
});
// Pass history to Router.
<Router history={history}>
...
</Router>
Even better if you create the history object as a module, so you can easily import it anywhere you may need it (e.g. import history from './history';
This is an old question and I don't quite understand the business need of listening for route changes to push a route change; seems roundabout.
BUT if you ended up here because all you wanted was to update the 'page_path' on a react-router route change for google analytics / global site tag / something similar, here's a hook you can now use. I wrote it based on the accepted answer:
useTracking.js
import { useEffect } from 'react'
import { useHistory } from 'react-router-dom'
export const useTracking = (trackingId) => {
const { listen } = useHistory()
useEffect(() => {
const unlisten = listen((location) => {
// if you pasted the google snippet on your index.html
// you've declared this function in the global
if (!window.gtag) return
window.gtag('config', trackingId, { page_path: location.pathname })
})
// remember, hooks that add listeners
// should have cleanup to remove them
return unlisten
}, [trackingId, listen])
}
You should use this hook once in your app, somewhere near the top but still inside a router. I have it on an App.js that looks like this:
App.js
import * as React from 'react'
import { BrowserRouter, Route, Switch } from 'react-router-dom'
import Home from './Home/Home'
import About from './About/About'
// this is the file above
import { useTracking } from './useTracking'
export const App = () => {
useTracking('UA-USE-YOURS-HERE')
return (
<Switch>
<Route path="/about">
<About />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
)
}
// I find it handy to have a named export of the App
// and then the default export which wraps it with
// all the providers I need.
// Mostly for testing purposes, but in this case,
// it allows us to use the hook above,
// since you may only use it when inside a Router
export default () => (
<BrowserRouter>
<App />
</BrowserRouter>
)
I came across this question as I was attempting to focus the ChromeVox screen reader to the top of the "screen" after navigating to a new screen in a React single page app. Basically trying to emulate what would happen if this page was loaded by following a link to a new server-rendered web page.
This solution doesn't require any listeners, it uses withRouter() and the componentDidUpdate() lifecycle method to trigger a click to focus ChromeVox on the desired element when navigating to a new url path.
Implementation
I created a "Screen" component which is wrapped around the react-router switch tag which contains all the apps screens.
<Screen>
<Switch>
... add <Route> for each screen here...
</Switch>
</Screen>
Screen.tsx Component
Note: This component uses React + TypeScript
import React from 'react'
import { RouteComponentProps, withRouter } from 'react-router'
class Screen extends React.Component<RouteComponentProps> {
public screen = React.createRef<HTMLDivElement>()
public componentDidUpdate = (prevProps: RouteComponentProps) => {
if (this.props.location.pathname !== prevProps.location.pathname) {
// Hack: setTimeout delays click until end of current
// event loop to ensure new screen has mounted.
window.setTimeout(() => {
this.screen.current!.click()
}, 0)
}
}
public render() {
return <div ref={this.screen}>{this.props.children}</div>
}
}
export default withRouter(Screen)
I had tried using focus() instead of click(), but click causes ChromeVox to stop reading whatever it is currently reading and start again where I tell it to start.
Advanced note: In this solution, the navigation <nav> which inside the Screen component and rendered after the <main> content is visually positioned above the main using css order: -1;. So in pseudo code:
<Screen style={{ display: 'flex' }}>
<main>
<nav style={{ order: -1 }}>
<Screen>
If you have any thoughts, comments, or tips about this solution, please add a comment.
import React from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import Sidebar from './Sidebar';
import Chat from './Chat';
<Router>
<Sidebar />
<Switch>
<Route path="/rooms/:roomId" component={Chat}>
</Route>
</Switch>
</Router>
import { useHistory } from 'react-router-dom';
function SidebarChat(props) {
**const history = useHistory();**
var openChat = function (id) {
**//To navigate**
history.push("/rooms/" + id);
}
}
**//To Detect the navigation change or param change**
import { useParams } from 'react-router-dom';
function Chat(props) {
var { roomId } = useParams();
var roomId = props.match.params.roomId;
useEffect(() => {
//Detect the paramter change
}, [roomId])
useEffect(() => {
//Detect the location/url change
}, [location])
}
Use the useLocation() Hook to detect the URL change and put it in dependency array in useEffect() this trick worked for me
const App = () => {
const location = useLocation();
useEffect(() => {
window.scroll(0,0);
}, [location]);
return (
<React.Fragment>
<Routes>
<Route path={"/"} element={<Template/>} >
<Route index={true} element={<Home/>} />
<Route path={"cart"} element={<Cart/>} />
<Route path={"signin"} element={<Signin/>} />
<Route path={"signup"} element={<Signup/>} />
<Route path={"product/:slug"} element={<Product/>} />
<Route path={"category/:category"} element={<ProductList/>} />
</Route>
</Routes>
</React.Fragment>
);
}
export default App;
You can use the useLocation with componentDidUpdate for getting the route change for class component and useEffect for functional component
In Class component
import { useLocation } from "react-router";
class MainApp extends React.Component {
constructor(props) {
super(props);
}
async componentDidUpdate(prevProps) {
if(this.props.location.pathname !== prevProps.location.pathname)
{
//route has been changed. do something here
}
}
}
function App() {
const location = useLocation()
return <MainApp location={location} />
}
In functional component
function App() {
const location = useLocation()
useEffect(() => {
//route change detected. do something here
}, [location]) //add location in dependency. It detects the location change
return <Routes>
<Route path={"/"} element={<Home/>} >
<Route path={"login"} element={<Login/>} />
</Routes>
}
React Router V5
If you want the pathName as a string ('/' or 'users'), you can use the following:
// React Hooks: React Router DOM
let history = useHistory();
const location = useLocation();
const pathName = location.pathname;

React Router 4 dynamic components loaded on the fly from database

I'm migrating to React Router 4.11 from 2.5.1. In the older version, my ISOMORPHIC/UNIVERSAL app retrieves the routes/components dynamically from a database at startup and I want to retain that same functionality with 4.11.
In the 2.5.1 version of react-router, I was able to load routes & components dynamically from the database by calling _createRouter with the routes parameter being the data retrieved from my db as such:
////////////////////////
// from client entry.js
////////////////////////
async function main() {
const result = await _createRouter(routes)
ReactDOM.render(
<Provider store={store} key="provider">
<Router history={history}>
{result}
</Router>
</Provider>
dest
);
}
//////////////////////
// from shared modules
//////////////////////
function _createRouter(routes) {
const childRoutes = [ // static routes, ie Home, ForgotPassword, etc];
////////////////////////
// crucial part here
////////////////////////
routes.map(route => {
const currComponent = require('../../client/components/' + route.route + '.js');
const obj = {
path: route.route,
component: currComponent
};
childRoutes.push(obj)
});
childRoutes.push({path: '*', component: NotFoundComponent});
return { path: '', component: .APP, childRoutes: childRoutes };
}
I am frustrated that I cannot find any docs on performing the same event in 4.11. Every example shows the routes hard coded like so:
render() {
return (
<Route path='/' component={Home} />
<Route path='/home' component={About} />
<Route path='/topics' component={Topics} />
)
}
This does not appear real-world to me especially for large apps.
Can anyone point me in the direction of accomplishing the same success I had in 2.5.1 with 4.11 insofar as being able to dynamically load my routes/components on the fly from a database?
Thanks much !
Maybe this code is the solution to this issue. "dynamic route import component"
import React, {
Component
} from "react";
const Containers = () => ( < div className = "containers" > Containers < /div>);
class ComponentFactory extends Component{constructor(props){super(props);this.state={RouteComponent:Containers}}
componentWillMount() {
const name = this.props.page;
import ('../pages/' + name + '/index').then(RouteComponent => {
return this.setState({
RouteComponent: RouteComponent.default
})
}).catch(err => console.log('Failed to load Component', err))
}
render() {
const {RouteComponent } = this.state;
return (<RouteComponent { ...this.props }/> );
}
}
export default ComponentFactory
<Route path="/:page"
render={rest => <ComponentFactory
page={rest.match.params.page}
{...rest}/>}/>
In the new version, each route is just a regular react component. Therefore, you should be able to dynamically create routes as you'd create any other component.
render() {
const { routes } = this.props;
return (
<div>
{
routes.map(route => <Route path={route.path} component={route.component} />)
}
</div>
);
}
You will also need to dynamically import your components before trying to render the routes.

Nested route using getComponent does not render

I've been building a React app with a react-router 3.x setup like this:
<Route component={Global}>
<Route path="/" getComponent={(loc, cb) => loadRoute('Home', cb)} />
<Route path="/app" component={App}>
<IndexRoute
getComponent={(loc, cb) => loadRoute('AppHome', cb)}
/>
<Route
path="search"
getComponent={(loc, cb) => loadRoute('Search', cb)}
/>
</Route>
</Route>
I've had this working really well for chunking the containers within App. The loadRoute function wraps a System.import call adding containers/${name}/index.js.
However, adding a dynamic getComponent={loadRoute(...)} to App and navigating to /app causes Global to have no children; the JS chunks for App and AppHome are downloaded but nothing is mounted.
The loadRoute function:
const loadRoute = (container, callback) => {
return System
.import(`containers/${container}/index.js`)
.then(module => callback(null, module.default))
.catch(errorLoading);
};
For anybody seeing this question and being in the same position. I solved this a few months ago.
I used component instead of getComponent and passed it a component that is in charge of asynchronously loading the chunk, displaying a loading spinner and rendering it when available. The react-async-component package would also solve this issue.
Now, chunked routes can be nested many levels deep allowing for true code splitting in react-router 3.x.
components/AsyncComponent.js
import React from 'react';
const defaultLoadingElement = <div>Loading spinner...</div>;
const asyncComp = (getComponent, loadingElement = defaultLoadingElement) => {
return class AsyncComponent extends React.Component {
static Component = null;
state = { Component: AsyncComponent.Component };
componentWillMount() {
if(!this.state.Component) {
getComponent().then(Component => {
AsyncComponent.Component = Component;
this.setState({ Component });
});
}
}
render() {
const { Component } = this.state;
if(Component) {
return <Component {...this.props} />;
}
return loadingElement;
}
};
};
export default asyncComp;
routes.js
import React from 'react';
import { IndexRoute, Route } from 'react-router';
import asyncComp from 'components/AsyncComponent';
const loadRoute = container => asyncComp(() =>
import(`containers/${container}/index.js`)
.then(module => module.default)
);
const errorLoading = err => {
const error = new Error();
console.log(error.stack, err.stack);
throw new Error(`Dynamic page loading failed: ${err}`);
};
export default store => {
return (
<Route path="/admin" component={loadRoute('Admin')}>
<IndexRoute component={loadRoute('Admin/Dashboard')} />
<Route
path="accounts"
component={loadRoute('Admin/Accounts')}
/>
</Route>
);
};

How to test react-router with enzyme

I am using enzyme+mocha+chai to test my react-redux project. Enzyme provides shallow to test component behavior. But I didn't find a way to test the router. I am using react-router as below:
<Router history={browserHistory}>
...
<Route path="nurse/authorization" component{NurseAuthorization}/>
...
</Route>
I want to test this route nurse/authorization refer to NurseAuthorization component. How to test it in reactjs project?
EDIT1
I am using react-router as the router framework.
You can wrap your router inside a component in order to test it.
Routes.jsx
export default props => (
<Router history={browserHistory}>
...
<Route path="nurse/authorization" component{NurseAuthorization}/>
...
</Route>
)
index.js
import Routes from './Routes.jsx';
...
ReactDOM.render(<Routes />, document.getElementById('root'));
Then you have to shallow render your Routes component, and you are able to create an object map to check the correspondance between path and related component.
Routes.test.js
import { shallow } from 'enzyme';
import { Route } from 'react-router';
import Routes from './Routes.jsx';
import NurseAuthorization from './NurseAuthorization.jsx';
it('renders correct routes', () => {
const wrapper = shallow(<Routes />);
const pathMap = wrapper.find(Route).reduce((pathMap, route) => {
const routeProps = route.props();
pathMap[routeProps.path] = routeProps.component;
return pathMap;
}, {});
// { 'nurse/authorization' : NurseAuthorization, ... }
expect(pathMap['nurse/authorization']).toBe(NurseAuthorization);
});
EDIT
In case you want to additionally handle the case of render props:
const pathMap = wrapper.find(Route).reduce((pathMap, route) => {
const routeProps = route.props();
if (routeProps.component) {
pathMap[routeProps.path] = routeProps.component;
} else if (routeProps.render) {
pathMap[routeProps.path] = routeProps.render({}).type;
}
return pathMap;
}, {});
It will work only in case you render directly the component you want to test (without extra wrapper).
<Route path="nurse/authorization" render{() => <NurseAuthorization />}/>
I had my paths defined in another file for the dynamic router, so I am also testing that all the routes I am rendering as Routes are defined in my paths.js constants:
it('Routes should only have paths declared in src/routing/paths.js', () => {
const isDeclaredInPaths = (element, index, array) => {
return pathsDefined.indexOf(array[index]) >= 0;
}
expect(routesDefined.every(isDeclaredInPaths)).to.be.true;
});
This will only pass if the component is rendered successfully:
It works with Redux and react-router including hooks.
import React from "react";
import { expect } from "chai";
import { mount } from "enzyme";
import { MemoryRouter, Route } from "react-router-dom";
import { createMockStore } from "redux-test-utils";
import { Provider } from "react-redux";
...
describe("<MyComponent />", () => {
it("renders the component", () => {
let props = {
index: 1,
value: 1
};
let state = {};
const wrapper = mount(
<Provider store={createMockStore(state)}>
<MemoryRouter initialEntries={["/s/parameter1"]}>
<Route path="/s/:camera">
<MyComponent {...props} />
</Route>
</MemoryRouter>
</Provider>
);
expect(wrapper.find(ProcessedFrames.WrappedComponent)).to.have.lengthOf(1);
});
});
Tested for react-router-dom v6
Based on #Freez 's answer, I implemented a recursive function that returns a correct url map even if you are using nested routes.
You just need to add this once in setupTests.js for jest tests to be able to use it in any test:
function recursiveGetPathMap(route, parentPath){
let pathMap = {};
const routeProps = route.props();
let path = parentPath + (parentPath.length == 0 || parentPath[parentPath.length-1] == '/' ? '' : '/') + routeProps.path;
pathMap[path] = routeProps.element.type;
route.children(Route).forEach(el=>{
pathMap = {...pathMap, ...recursiveGetPathMap(el, path)};
});
return pathMap;
}
global.getPathMap = (wrapper)=>{
let pathMap = {};
wrapper.find(Routes).children(Route).forEach(el =>{
pathMap = {...pathMap, ...recursiveGetPathMap(el, "")};
});
return pathMap;
}
Example:
App.js
...
<Routes>
<Route path="/" element={<Layout/>}>
<Route path="users" element={<Users/>}>
<Route path=":name" element={<Profile/>}/>
</Route>
</Route>
</Routes>
...
App.test.js
...
it('whatever', ()=>{
const component = <App/>;
const wrapper = shallow(component);
const pathMap = getPathMap(wrapper);
expect(pathMap['/']).toBe(Layout);
expect(pathMap['/users']).toBe(Users);
expect(pathMap['/users/:name']).toBe(Profile);
});
...
The output of console.log(pathMap) in that example is:
{
'/': [Function: Layout],
'/users': [Function: Users],
'/users/:name': [Function: Profile]
}
Note that if you have a route without path (index route):
<Route index element={<SomeComponent/>}/>
the route will be like /somepath/somepath/undefined

Resources