I am trying to test a component which listens to the history being changed by creating a wrapper in Enzyme which places my component within a MemoryRouter; i.e.:
mount(
<MemoryRouter initialEntries={'/path/param1}>
<Switch>
<Route
path="/path"
component={MyComponent}
/>
</Switch>
</MemoryRouter>
)
For the initial path, this works fine, however, I specifically want to test what happens when it starts at say /path/param1 but then the history is changed to /path/param2
The monitoring of the path is being done by wrapping the component's export with withRouter, like this:
export default withRouter(MyComponent)
And then upon construction, I am using history.listen to subscribe to the history changes.
You can use a base <Route> component with a custom history object to do this.
First, install the history package (to devDevpendencies if you are only using it in tests).
In your test file:
// we are using memory history only
import { createMemoryHistory } from 'history';
import { Router, Route} from 'react-router-dom';
Then, setup your component like so: (you can drop the Switch if there is only one route)
const history = createMemoryHistory({
initialEntries: ['/path/param1'],
});
// mount with custom history object
mount(
<Router history={history}>
<Route
path="/path"
component={MyComponent}
/>
</Router>
)
Later in your test, you can check what's the current location of the history after your component does its thing.
// check current path has been changed
expect(history.entries[0].pathname).to.eq('/path/param2');
You can find all the other stuff you can get access to with history here:
https://github.com/ReactTraining/history
Related
Following problem:
const history = createBrowserHistory()
render() {
return (
<Router history={history}>
<WButton history={history}/>
<Switch>
<Route exact path='/' component={Home}/>
<Route exact path='/next' component={Next}/>
When I do this.props.history.push("/next") in WButton Component it only updates the url not switches to Next Component. When I move WButton into Home Component everything works correctly but I'd like to keep WButton on top level. Is this somehow possible?
With createBrowserHistory({ forceRefresh: true }) it also works on top level but I don't want to reload page with each navigation step.
Found the solution:
I could just use export default withRouter(WButton) in the WButton Component instead of the standard export and then remove the manual injection of the history via the props.
Import is import { withRouter } from 'react-router-dom';
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 two apps within single-spa, one in React and other in Vue.
The React app uses history library for navigation. Below given are my React app files:
history.js
import { createBrowserHistory } from 'history'
export const history = createBrowserHistory({
basename: '/myapp,
forceRefresh: true
})
App.js
import React, {Component} from 'react';
import { Router, Switch, Route } from 'react-router-dom';
import history from ‘../history.js’;
class App extends Component {
constructor(props){
super(props);
}
render(){
return (
<Router history={history}>
<Switch>
<Route exact path="/user" component={User} />
<Route exact path="/" component={Home} />
</Switch>
<Router />
)
}
}
I face an issue when:
I’m at the path https://localhost:3000/myapp/user of React app and I switch to the Vue app (using navbar menu).
Now when I switch back to React app, the url shows https://localhost:3000/myapp which ideally should load my Home component.
But now the history still has the old location (https://localhost:3000/myapp/user) which then loads the old User component.
Is there a way to update history when the url changes?
I didn't find how to change history, but I want you to fix some grammar errors. Meybe fixing errors can help you.
history.js
export const history = createBrowserHistory({
basename: '/myapp, // basename: '/myapp',
forceRefresh: true
})
App.js
<Switch>
<Route exact path="/user" component={User} /> // <Route path="/user" component={User} />
<Route exact path="/" component={Home} />
</Switch>
I think this boils down to getting to routing mechanisms to work together. This is an interesting problem and I have two ideas that could work:
Idea 1: refresh the page
If you're okay with a full reload, refreshing the browser whenever you switch between Vue and React would probably reset the state in both routers so that they both read the initial state they loaded on and continue.
I would do this by assigning window.location.href = '/myapp' inside any event handler that would cause the transition to React. It should reload the browser and navigate to that page. This should work if your react app is loading at that path.
Idea 2: Use createMemoryHistory and sync it with Vue's router
Instead of feeding react-router a "browser router", you can instead pass it a "memory router". As the name implies, the memory router holds state in memory (instead of using the History API).
I'm thinking you can wire up the events from Vue's router to react-router's memory router and get them in sync like that. I think you can call history.push inside of Vue's global after hooks.
Something like this:
// 🔴 I have not tested this, take this as inspiration
const router = new VueRouter({/* ... */});
const history = createMemoryHistory({/* ... */});
router.afterEach(({fullPath}) => {
history.replace(fullPath, {fromVueRouter: true});
});
history.listen(location => {
if (location.state?.fromVueRouter) return;
router.replace(location.pathname);
});
Versions -
"react-router": "5.0.0",
"react-router-dom": "5.0.0"
In my app container component, I am using withRouter to access location and history props. I use it like -
export default withRouter(connect(mapStateToProps)(AppContainerComponent));
The result is, I get a blank page. No errors, just a blank page.
If I remove the withRouter HOC, it works.
Also, it used to work in v4.0.0-beta.8.
I import the withRouter as -
import { withRouter } from 'react-router';
Not sure what the problem is.
Note - I have gone through this link which talks about how the shouldCOmponentUpdate method does not take into account the context changes (which react-router uses now), and it suggests to wrap the component with 'withRouter' HOC, but it is itself not working for me.
Update -
Following is the route path I am using -
index.js -
<Provider store={store}>
<BrowserRouter>
<AppContainerComponent />
</BrowserRouter>
</Provider>
appcontainer.component (its redux connected) renders following component -
<AppRoutes isAuthenticated={isAuthenticated} />
appRoutes.component.ts - This component renders the 'UnauthenticatedRoute' and 'AuthenticatedRoute' custom HOC like -
<UnauthenticatedRoute
path="/"
exact
component={SignupComponent}
isAuthenticated={isAuthenticated} />
<AuthenticatedRoute
path="/app"
exact
component={AppComponent}
isAuthenticated={isAuthenticated} />
The 'UnauthenticatedRoute' and 'AuthenticatedRoute' HOC will render the passed component if the user has been successfully authenticated, otherwise it will redirect to '/signup' path using react-router's Redirect component.
The AppComponent has bunch of routes defined by Route component as -
<Route exact
path="/path1"
render={}
/>
Another update -
Came across this link which talks exactly about the problem I am facing, but the solution - using withRouter. Maybe I am not importing something from right location? Something similar here
I am using react-router and react-router-dom with preact (8.4.2), preact-cli (v2.2.1) and preact-compact (3.18.4). I am not sure if this is the root cause? Isn't the react-router supported out of the box for preact?
I have set up this link to demonstrate the issue.(issue_example branch)
Try wrapping just AppContainerComponent with your withRouter, like this:
export default connect(mapStateToProps)(withRouter(AppContainerComponent));
I managed to solve the issue by creating custom withRouter hoc which takes in a Component to render and wraps that component inside a Router component. Router component has the props I need (history, location) and I pass them down to the component I am rendering, eliminating the use of RRD's withRouter completely.
import { Route } from 'react-router-dom';
const withRouter = (ConnectedComponent) => {
const witRouterComponent = (props) => (
<Route render={routeProps =>
<ConnectedComponent {...routeProps} {...props} />} />
);
return witRouterComponent;
};
export default withRouter;
To use it in (for ex. AppContainerComponent)
const ConnectedComponent = connect(mapStateToProps)(AppContainerComponent);
export default withRouter(ConnectedComponent);
More on this here.
I'm trying to set up a history listener at the topmost level so that every time the location changes, some code is executed (Google Analytics tracking in my case).
Currently, my App.js looks like this:
render() {
return (
<BrowserRouter>
<Switch>
<Route path="/" exact component={Home}/>
<Route path="/" component={MainLayout}/>
</Switch>
</BrowserRouter>
);
}
As I understand, I can only use history inside BrowserRouter, but it can only have one child, so I can only put it inside Switch, but then I'll have to put it inside every component under Switch, which I can't do since I want it to run only once. If I just add a custom component under Switch without wrapping it into a Router, there's no history.
What should I change to make it work? Is it possible to get an instance of history in the App.js code? Is it possible to achieve using a custom component or a HOC?
You need to add a Route which will get rendered unconditionally.
Try something like this:
render() {
return (
<BrowserRouter>
<React.Fragment>
<Route component={HistoryListenerComponent}/>
<Switch>
<Route path="/" exact component={Home}/>
<Route path="/" component={MainLayout}/>
</Switch>
</React.Fragment>
</BrowserRouter>
);
}
So HistoryListenerComponent is always rendered and you can listen from its componentDidMount.
Heres what worked for me:
import {withRouter} from 'react-router-dom';
Now just wrap the component where you are using React-ga with withRouter like this:
componentDidMount=()=>{
ReactGa.initialize('00000'); //enter id
ReactGa.pageview(window.location.pathname + window.location.search);
this.props.history.listen(location => {
ReactGa.set({ page: location.pathname }); // Update the user's current page
ReactGa.pageview(location.pathname); // Record a pageview for the given page
});
}
render (){
return (
<Switch>
<Route ....>
.....
<Switch>
);
}
export default withRouter(ComponentName)
Remember without wrapping withRouter , you cant use this.props.history property.
You cant use custom history listener to BrowserRouter component. It has an inbuilt history prop (i.e this.props.history).
It is possible using history package, instead of relying on the default one created by react router.
For instance, using history.listen:
history.listen((location) => {
ReactGA.pageview(location.pathname + window.location.search);
});
In my exemple, I use react-ga, as a way to standardize our GA Instrumentation across projects.
Found out that the easiest option is the Update component -- https://github.com/pshrmn/rrc/blob/master/docs/OnUpdate.md