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;
Related
I am in /customerOrders/13 page and from there I try to redirect to /customerOrders/14 using navigate('/customerOrders/14'). Even though the URL is updated, page is not redirected to /customerOrders/14.
Below are code fragments I extracted related to this from the codebase.
App.js
import {
BrowserRouter,
Routes,
Route
} from "react-router-dom";
...
<BrowserRouter>
<Routes>
<Route path="customerOrders/:id" element={<CustomerOrderForm />}></Route>
</Routes>
<Router>
CustomerOrderForm.jsx
import { useNavigate } from "react-router-dom";
...
const CustomerOrderForm = () => {
let navigate = useNavigate();
const save = async () => {
//
// logic to persist data goes here...
//
navigate(`/customerOrders/${customerOrderId}`);
}
...
}
When you are on a given route:
<Route path="customerOrders/:id" element={<CustomerOrderForm />} />
and navigating to the same route already rendering the mounted component then the component needs to "listen" for changes to the route, in this case, specifically the id route match param that is updated. Use an useEffect hook with a dependency on the id route match param to rerun any logic depending on it.
import { useNavigate, useParams } from "react-router-dom";
...
const CustomerOrderForm = () => {
const navigate = useNavigate();
const { id } = useParams();
useEffect(() => {
// rerun logic depending on id value
}, [id]);
const save = async () => {
//
// logic to persist data goes here...
//
navigate(`/customerOrders/${customerOrderId}`);
};
...
};
The useHistory() hook is not working in my project. I have it in different components but none of them work.
I am using "react-router-dom": "^5.2.0",
import {useHistory} from 'react-router-dom'
const history = useHistory()
const logout = () => {
toggleMenu()
setUser(null)
dispatch({type: 'LOGOUT'})
history.push('/')
}
And also in action its not working
export const signin = (formdata, history, setError) => async (dispatch) => {
try {
const {data} = await API.signIn(formdata)
if(data.error){
setError(data.message)
}else{
dispatch({type: SIGNIN, data})
history.push('/dashboard')
}
} catch (error) {
setError(error)
console.log(error)
}
}
Here is my router.js file
const Router = () => {
const user = JSON.parse(localStorage.getItem('profile'))
return (
<BrowserRouter>
<>
{user && <Navigation/>}
<Switch>
<Route path='/page-not-found' component={Auth}/>
<Route path='/' exact component={()=>((user && user.result) ? <Redirect to="/dasboard"/> : <Auth/>)} />
<Route path='/dashboard' component={() => (user ? <Dashboard/> : <Redirect to='/'/>)} />
<Route path='/books-purchase' component={() => (user ? <BooksPage/> : <Redirect to='/' />)} />
</Switch>
</>
</BrowserRouter>
)
}
For the version of react router dom less than 6.^
You can use useHistory() hook like your code is showing
For the latest version of react router dom greater than 6.^
You can use useNavigate() hook like this. You can also use without props
import { useNavigate } from 'react-router-dom';
class Login extends Component {
....
let navigate = useNavigate();
navigate('/');
.....
Instead of useHistory, try useNavigate:
import { useNavigate } from 'react-router-dom';
After some investigation, I found that there is a bug in react-router-dom version ^5.2.0. See this and this . I would suggest you to downgrade react-router-dom version to 4.10.1
In my point of view, It doesn't need to downgrade react-router-dom, just use "useNavigate" hook instead of "useHistory" hook. For example if you also want to send some data to your dashboard page, you can use it like so:
import { useNavigate } from 'react-router-dom'
const FunctionalComponent = () => {
const navigate = useNavigate();
const TestHandler = (someData) => {
navigate("/dashboard", {state: {someData: someData}});
//and if you don't want to send any data use like so:
//navigate("/dashboard");
}
}
Finally, if you want to use that sent data for example in your dashboard page, you can do it like so:
import { useLocation } from 'react-router-dom'
export const Dashboard = () => {
const location = useLocation();
useEffect(() => {
if (location.state.someData) {
// "someData" is available here.
}
});
You can refer to this migration guide: https://reactrouter.com/docs/en/v6/upgrading/v5
i have same error, I forgot add type to button that working function. when i give type,problem was solved
Sometimes, if we call history in dialog or modal it shows error, pass the method to components
const methodTopass= () => {
history.push("/route");
};
<componet methodTopass/>
left "react-router-dom": "^5.2.0", add "history": "4.10.1", in to package.json
then use import { createBrowserHistory } from "history";
and
let hist = createBrowserHistory();
hist.push("/domoy");
I am developing an application that uses the default React code spltting using the Lazy/Suspense approach and the React Router for component rendering. Currently, when I navigate to another path, if the network speed is slow, the path is updated and the fallback component is rendered while the component is fetched, is there any way to wait on the current path until the component package is completely downloaded?
Yes, in concurrent mode, where useTransition() is enabled, you can create a custom router to wrap each of the navigation methods on your history object in a suspense transition:
import { useState, unstable_useTransition as useTransition } from 'react';
import { Router } from 'react-router-dom';
const SuspenseRouter = ({ children, history, ...config }) => {
const [startTransition, isPending] = useTransition(config);
const [suspenseHistory] = useState(() => {
const { push, replace, go } = history;
history.push = (...args) => {
startTransition(() => { push.apply(history, args); });
};
history.replace = (...args) => {
startTransition(() => { replace.apply(history, args); });
};
history.go = (...args) => {
startTransition(() => { go.apply(history, args); });
};
});
suspenseHistory.isPending = isPending;
return (
<Router history={suspenseHistory}>
{children}
</Router>
);
};
export default SuspenseRouter;
Example usage might look something like this:
import { Suspense, lazy, unstable_createRoot as createRoot } from 'react';
import { Switch, Route } from 'react-router-dom';
import { createBrowserHistory } from 'history';
import SuspenseRouter from './components/SuspenseRouter';
const history = createBrowserHistory();
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const App = () => (
<SuspenseRouter history={history} timeoutMs={2000}>
<Suspense fallback="Loading...">
<Switch>
<Route path="/" exact={true} component={Home} />
<Route path="/about" component={About} />
</Switch>
</Suspense>
</SuspenseRouter>
);
createRoot(document.getElementById('root')).render(<App />);
Set timeoutMs to Infinity if you want to wait indefinitely on the previous route. In the example above, setting it to 2000 should wait on the previous route for up to 2 seconds, then display the fallback if the code for the requested route hasn't downloaded by then.
Here is another option: instead of suspending url change you can suspend screen change.
Package react-router-loading allows to show loading bar and fetch some data before switching the screen.
Just use Switch and Route from this package instead of react-router-dom:
import { Switch, Route } from "react-router-loading";
Add loading props to the Route where you want to wait something:
<Route path="/my-component" component={MyComponent} loading/>
And then somewhere at the end of fetch logic in MyComponent add loadingContext.done();:
import { LoadingContext } from "react-router-loading";
const loadingContext = useContext(LoadingContext);
const loading = async () => {
//fetching some data
//call method to indicate that fetching is done and we are ready to switch
loadingContext.done();
};
In the current version of React Router (v3) I can accept a server response and use browserHistory.push to go to the appropriate response page. However, this isn't available in v4, and I'm not sure what the appropriate way to handle this is.
In this example, using Redux, components/app-product-form.js calls this.props.addProduct(props) when a user submits the form. When the server returns a success, the user is taken to the Cart page.
// actions/index.js
export function addProduct(props) {
return dispatch =>
axios.post(`${ROOT_URL}/cart`, props, config)
.then(response => {
dispatch({ type: types.AUTH_USER });
localStorage.setItem('token', response.data.token);
browserHistory.push('/cart'); // no longer in React Router V4
});
}
How can I make a redirect to the Cart page from function for React Router v4?
You can use the history methods outside of your components. Try by the following way.
First, create a history object used the history package:
// src/history.js
import { createBrowserHistory } from 'history';
export default createBrowserHistory();
Then wrap it in <Router> (please note, you should use import { Router } instead of import { BrowserRouter as Router }):
// src/index.jsx
// ...
import { Router, Route, Link } from 'react-router-dom';
import history from './history';
ReactDOM.render(
<Provider store={store}>
<Router history={history}>
<div>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/login">Login</Link></li>
</ul>
<Route exact path="/" component={HomePage} />
<Route path="/login" component={LoginPage} />
</div>
</Router>
</Provider>,
document.getElementById('root'),
);
Change your current location from any place, for example:
// src/actions/userActionCreators.js
// ...
import history from '../history';
export function login(credentials) {
return function (dispatch) {
return loginRemotely(credentials)
.then((response) => {
// ...
history.push('/');
});
};
}
UPD: You can also see a slightly different example in React Router FAQ.
React Router v4 is fundamentally different from v3 (and earlier) and you cannot do browserHistory.push() like you used to.
This discussion seems related if you want more info:
Creating a new browserHistory won't work because <BrowserRouter> creates its own history instance, and listens for changes on that. So a different instance will change the url but not update the <BrowserRouter>.
browserHistory is not exposed by react-router in v4, only in v2.
Instead you have a few options to do this:
Use the withRouter high-order component
Instead you should use the withRouter high order component, and wrap that to the component that will push to history. For example:
import React from "react";
import { withRouter } from "react-router-dom";
class MyComponent extends React.Component {
...
myFunction() {
this.props.history.push("/some/Path");
}
...
}
export default withRouter(MyComponent);
Check out the official documentation for more info:
You can get access to the history object’s properties and the closest <Route>'s match via the withRouter higher-order component. withRouter will re-render its component every time the route changes with the same props as <Route> render props: { match, location, history }.
Use the context API
Using the context might be one of the easiest solutions, but being an experimental API it is unstable and unsupported. Use it only when everything else fails. Here's an example:
import React from "react";
import PropTypes from "prop-types";
class MyComponent extends React.Component {
static contextTypes = {
router: PropTypes.object
}
constructor(props, context) {
super(props, context);
}
...
myFunction() {
this.context.router.history.push("/some/Path");
}
...
}
Have a look at the official documentation on context:
If you want your application to be stable, don't use context. It is an experimental API and it is likely to break in future releases of React.
If you insist on using context despite these warnings, try to isolate your use of context to a small area and avoid using the context API directly when possible so that it's easier to upgrade when the API changes.
Now with react-router v5 you can use the useHistory hook like this:
import { useHistory } from "react-router-dom";
function HomeButton() {
let history = useHistory();
function handleClick() {
history.push("/home");
}
return (
<button type="button" onClick={handleClick}>
Go home
</button>
);
}
read more at: https://reacttraining.com/react-router/web/api/Hooks/usehistory
Simplest way in React Router 4 is to use
this.props.history.push('/new/url');
But to use this method, your existing component should have access to history object. We can get access by
If your component is linked to Route directly, then your component already has access to history object.
eg:
<Route path="/profile" component={ViewProfile}/>
Here ViewProfile has access to history.
If not connected to Route directly.
eg:
<Route path="/users" render={() => <ViewUsers/>}
Then we have to use withRouter, a heigher order fuction to warp the existing component.
Inside ViewUsers component
import { withRouter } from 'react-router-dom';
export default withRouter(ViewUsers);
That's it now, your ViewUsers component has access to history object.
UPDATE
2- in this scenario, pass all route props to your component, and then we can access this.props.history from the component even without a HOC
eg:
<Route path="/users" render={props => <ViewUsers {...props} />}
This is how I did it:
import React, {Component} from 'react';
export default class Link extends Component {
constructor(props) {
super(props);
this.onLogout = this.onLogout.bind(this);
}
onLogout() {
this.props.history.push('/');
}
render() {
return (
<div>
<h1>Your Links</h1>
<button onClick={this.onLogout}>Logout</button>
</div>
);
}
}
Use this.props.history.push('/cart'); to redirect to cart page it will be saved in history object.
Enjoy, Michael.
According to React Router v4 documentation - Redux Deep Integration session
Deep integration is needed to:
"be able to navigate by dispatching actions"
However, they recommend this approach as an alternative to the "deep integration":
"Rather than dispatching actions to navigate you can pass the history object provided to route components to your actions and navigate with it there."
So you can wrap your component with the withRouter high order component:
export default withRouter(connect(null, { actionCreatorName })(ReactComponent));
which will pass the history API to props. So you can call the action creator passing the history as a param. For example, inside your ReactComponent:
onClick={() => {
this.props.actionCreatorName(
this.props.history,
otherParams
);
}}
Then, inside your actions/index.js:
export function actionCreatorName(history, param) {
return dispatch => {
dispatch({
type: SOME_ACTION,
payload: param.data
});
history.push("/path");
};
}
Nasty question, took me quite a lot of time, but eventually, I solved it this way:
Wrap your container with withRouter and pass history to your action in mapDispatchToProps function. In action use history.push('/url') to navigate.
Action:
export function saveData(history, data) {
fetch.post('/save', data)
.then((response) => {
...
history.push('/url');
})
};
Container:
import { withRouter } from 'react-router-dom';
...
const mapDispatchToProps = (dispatch, ownProps) => {
return {
save: (data) => dispatch(saveData(ownProps.history, data))}
};
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Container));
This is valid for React Router v4.x.
I offer one more solution in case it is worthful for someone else.
I have a history.js file where I have the following:
import createHistory from 'history/createBrowserHistory'
const history = createHistory()
history.pushLater = (...args) => setImmediate(() => history.push(...args))
export default history
Next, on my Root where I define my router I use the following:
import history from '../history'
import { Provider } from 'react-redux'
import { Router, Route, Switch } from 'react-router-dom'
export default class Root extends React.Component {
render() {
return (
<Provider store={store}>
<Router history={history}>
<Switch>
...
</Switch>
</Router>
</Provider>
)
}
}
Finally, on my actions.js I import History and make use of pushLater
import history from './history'
export const login = createAction(
...
history.pushLater({ pathname: PATH_REDIRECT_LOGIN })
...)
This way, I can push to new actions after API calls.
Hope it helps!
this.context.history.push will not work.
I managed to get push working like this:
static contextTypes = {
router: PropTypes.object
}
handleSubmit(e) {
e.preventDefault();
if (this.props.auth.success) {
this.context.router.history.push("/some/Path")
}
}
Be careful that don't use react-router#5.2.0 or react-router-dom#5.2.0 with history#5.0.0. URL will update after history.push or any other push to history instructions but navigation is not working with react-router. use npm install history#4.10.1 to change the history version. see React router not working after upgrading to v 5.
I think this problem is happening when push to history happened. for example using <NavLink to="/apps"> facing a problem in NavLink.js that consume <RouterContext.Consumer>. context.location is changing to an object with action and location properties when the push to history occurs. So currentLocation.pathname is null to match the path.
In this case you're passing props to your thunk. So you can simply call
props.history.push('/cart')
If this isn't the case you can still pass history from your component
export function addProduct(data, history) {
return dispatch => {
axios.post('/url', data).then((response) => {
dispatch({ type: types.AUTH_USER })
history.push('/cart')
})
}
}
I struggled with the same topic.
I'm using react-router-dom 5, Redux 4 and BrowserRouter.
I prefer function based components and hooks.
You define your component like this
import { useHistory } from "react-router-dom";
import { useDispatch } from "react-redux";
const Component = () => {
...
const history = useHistory();
dispatch(myActionCreator(otherValues, history));
};
And your action creator is following
const myActionCreator = (otherValues, history) => async (dispatch) => {
...
history.push("/path");
}
You can of course have simpler action creator if async is not needed
Here's my hack (this is my root-level file, with a little redux mixed in there - though I'm not using react-router-redux):
const store = configureStore()
const customHistory = createBrowserHistory({
basename: config.urlBasename || ''
})
ReactDOM.render(
<Provider store={store}>
<Router history={customHistory}>
<Route component={({history}) => {
window.appHistory = history
return (
<App />
)
}}/>
</Router>
</Provider>,
document.getElementById('root')
)
I can then use window.appHistory.push() anywhere I want (for example, in my redux store functions/thunks/sagas, etc) I had hoped I could just use window.customHistory.push() but for some reason react-router never seemed to update even though the url changed. But this way I have the EXACT instance react-router uses. I don't love putting stuff in the global scope, and this is one of the few things I'd do that with. But it's better than any other alternative I've seen IMO.
If you are using Redux, then I would recommend using npm package react-router-redux. It allows you to dispatch Redux store navigation actions.
You have to create store as described in their Readme file.
The easiest use case:
import { push } from 'react-router-redux'
this.props.dispatch(push('/second page'));
Second use case with Container/Component:
Container:
import { connect } from 'react-redux';
import { push } from 'react-router-redux';
import Form from '../components/Form';
const mapDispatchToProps = dispatch => ({
changeUrl: url => dispatch(push(url)),
});
export default connect(null, mapDispatchToProps)(Form);
Component:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export default class Form extends Component {
handleClick = () => {
this.props.changeUrl('/secondPage');
};
render() {
return (
<div>
<button onClick={this.handleClick}/>
</div>Readme file
);
}
}
I was able to accomplish this by using bind(). I wanted to click a button in index.jsx, post some data to the server, evaluate the response, and redirect to success.jsx. Here's how I worked that out...
index.jsx:
import React, { Component } from "react"
import { postData } from "../../scripts/request"
class Main extends Component {
constructor(props) {
super(props)
this.handleClick = this.handleClick.bind(this)
this.postData = postData.bind(this)
}
handleClick() {
const data = {
"first_name": "Test",
"last_name": "Guy",
"email": "test#test.com"
}
this.postData("person", data)
}
render() {
return (
<div className="Main">
<button onClick={this.handleClick}>Test Post</button>
</div>
)
}
}
export default Main
request.js:
import { post } from "./fetch"
export const postData = function(url, data) {
// post is a fetch() in another script...
post(url, data)
.then((result) => {
if (result.status === "ok") {
this.props.history.push("/success")
}
})
}
success.jsx:
import React from "react"
const Success = () => {
return (
<div className="Success">
Hey cool, got it.
</div>
)
}
export default Success
So by binding this to postData in index.jsx, I was able to access this.props.history in request.js... then I can reuse this function in different components, just have to make sure I remember to include this.postData = postData.bind(this) in the constructor().
so the way I do it is:
- instead of redirecting using history.push, I just use Redirect component from react-router-dom
When using this component you can just pass push=true, and it will take care of the rest
import * as React from 'react';
import { Redirect } from 'react-router-dom';
class Example extends React.Component {
componentDidMount() {
this.setState({
redirectTo: '/test/path'
});
}
render() {
const { redirectTo } = this.state;
return <Redirect to={{pathname: redirectTo}} push={true}/>
}
}
Use Callback. It worked for me!
export function addProduct(props, callback) {
return dispatch =>
axios.post(`${ROOT_URL}/cart`, props, config)
.then(response => {
dispatch({ type: types.AUTH_USER });
localStorage.setItem('token', response.data.token);
callback();
});
}
In component, you just have to add the callback
this.props.addProduct(props, () => this.props.history.push('/cart'))
React router V4 now allows the history prop to be used as below:
this.props.history.push("/dummy",value)
The value then can be accessed wherever the location prop is available as
state:{value} not component state.
As we have a history already included in react router 5, we can access the same with reference
import React from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
function App() {
const routerRef = React.useRef();
const onProductNav = () => {
const history = routerRef.current.history;
history.push("product");
}
return (
<BrowserRouter ref={routerRef}>
<Switch>
<Route path="/product">
<ProductComponent />
</Route>
<Route path="/">
<HomeComponent />
</Route>
</Switch>
</BrowserRouter>
)
}
step one wrap your app in Router
import { BrowserRouter as Router } from "react-router-dom";
ReactDOM.render(<Router><App /></Router>, document.getElementById('root'));
Now my entire App will have access to BrowserRouter. Step two I import Route and then pass down those props. Probably in one of your main files.
import { Route } from "react-router-dom";
//lots of code here
//somewhere in my render function
<Route
exact
path="/" //put what your file path is here
render={props => (
<div>
<NameOfComponent
{...props} //this will pass down your match, history, location objects
/>
</div>
)}
/>
Now if I run console.log(this.props) in my component js file that I should get something that looks like this
{match: {…}, location: {…}, history: {…}, //other stuff }
Step 2 I can access the history object to change my location
//lots of code here relating to my whatever request I just ran delete, put so on
this.props.history.push("/") // then put in whatever url you want to go to
Also I'm just a coding bootcamp student, so I'm no expert, but I know you can also you use
window.location = "/" //wherever you want to go
Correct me if I'm wrong, but when I tested that out it reloaded the entire page which I thought defeated the entire point of using React.
Create a custom Router with its own browserHistory:
import React from 'react';
import { Router } from 'react-router-dom';
import { createBrowserHistory } from 'history';
export const history = createBrowserHistory();
const ExtBrowserRouter = ({children}) => (
<Router history={history} >
{ children }
</Router>
);
export default ExtBrowserRouter
Next, on your Root where you define your Router, use the following:
import React from 'react';
import { /*BrowserRouter,*/ Route, Switch, Redirect } from 'react-router-dom';
//Use 'ExtBrowserRouter' instead of 'BrowserRouter'
import ExtBrowserRouter from './ExtBrowserRouter';
...
export default class Root extends React.Component {
render() {
return (
<Provider store={store}>
<ExtBrowserRouter>
<Switch>
...
<Route path="/login" component={Login} />
...
</Switch>
</ExtBrowserRouter>
</Provider>
)
}
}
Finally, import history where you need it and use it:
import { history } from '../routers/ExtBrowserRouter';
...
export function logout(){
clearTokens();
history.push('/login'); //WORKS AS EXPECTED!
return Promise.reject('Refresh token has expired');
}
you can use it like this as i do it for login and manny different things
class Login extends Component {
constructor(props){
super(props);
this.login=this.login.bind(this)
}
login(){
this.props.history.push('/dashboard');
}
render() {
return (
<div>
<button onClick={this.login}>login</login>
</div>
)
/*Step 1*/
myFunction(){ this.props.history.push("/home"); }
/**/
<button onClick={()=>this.myFunction()} className={'btn btn-primary'}>Go
Home</button>
If you want to use history while passing a function as a value to a Component's prop, with react-router 4 you can simply destructure the history prop in the render attribute of the <Route/> Component and then use history.push()
<Route path='/create' render={({history}) => (
<YourComponent
YourProp={() => {
this.YourClassMethod()
history.push('/')
}}>
</YourComponent>
)} />
Note: For this to work you should wrap React Router's BrowserRouter Component around your root component (eg. which might be in index.js)
In the current version of React Router (v3) I can accept a server response and use browserHistory.push to go to the appropriate response page. However, this isn't available in v4, and I'm not sure what the appropriate way to handle this is.
In this example, using Redux, components/app-product-form.js calls this.props.addProduct(props) when a user submits the form. When the server returns a success, the user is taken to the Cart page.
// actions/index.js
export function addProduct(props) {
return dispatch =>
axios.post(`${ROOT_URL}/cart`, props, config)
.then(response => {
dispatch({ type: types.AUTH_USER });
localStorage.setItem('token', response.data.token);
browserHistory.push('/cart'); // no longer in React Router V4
});
}
How can I make a redirect to the Cart page from function for React Router v4?
You can use the history methods outside of your components. Try by the following way.
First, create a history object used the history package:
// src/history.js
import { createBrowserHistory } from 'history';
export default createBrowserHistory();
Then wrap it in <Router> (please note, you should use import { Router } instead of import { BrowserRouter as Router }):
// src/index.jsx
// ...
import { Router, Route, Link } from 'react-router-dom';
import history from './history';
ReactDOM.render(
<Provider store={store}>
<Router history={history}>
<div>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/login">Login</Link></li>
</ul>
<Route exact path="/" component={HomePage} />
<Route path="/login" component={LoginPage} />
</div>
</Router>
</Provider>,
document.getElementById('root'),
);
Change your current location from any place, for example:
// src/actions/userActionCreators.js
// ...
import history from '../history';
export function login(credentials) {
return function (dispatch) {
return loginRemotely(credentials)
.then((response) => {
// ...
history.push('/');
});
};
}
UPD: You can also see a slightly different example in React Router FAQ.
React Router v4 is fundamentally different from v3 (and earlier) and you cannot do browserHistory.push() like you used to.
This discussion seems related if you want more info:
Creating a new browserHistory won't work because <BrowserRouter> creates its own history instance, and listens for changes on that. So a different instance will change the url but not update the <BrowserRouter>.
browserHistory is not exposed by react-router in v4, only in v2.
Instead you have a few options to do this:
Use the withRouter high-order component
Instead you should use the withRouter high order component, and wrap that to the component that will push to history. For example:
import React from "react";
import { withRouter } from "react-router-dom";
class MyComponent extends React.Component {
...
myFunction() {
this.props.history.push("/some/Path");
}
...
}
export default withRouter(MyComponent);
Check out the official documentation for more info:
You can get access to the history object’s properties and the closest <Route>'s match via the withRouter higher-order component. withRouter will re-render its component every time the route changes with the same props as <Route> render props: { match, location, history }.
Use the context API
Using the context might be one of the easiest solutions, but being an experimental API it is unstable and unsupported. Use it only when everything else fails. Here's an example:
import React from "react";
import PropTypes from "prop-types";
class MyComponent extends React.Component {
static contextTypes = {
router: PropTypes.object
}
constructor(props, context) {
super(props, context);
}
...
myFunction() {
this.context.router.history.push("/some/Path");
}
...
}
Have a look at the official documentation on context:
If you want your application to be stable, don't use context. It is an experimental API and it is likely to break in future releases of React.
If you insist on using context despite these warnings, try to isolate your use of context to a small area and avoid using the context API directly when possible so that it's easier to upgrade when the API changes.
Now with react-router v5 you can use the useHistory hook like this:
import { useHistory } from "react-router-dom";
function HomeButton() {
let history = useHistory();
function handleClick() {
history.push("/home");
}
return (
<button type="button" onClick={handleClick}>
Go home
</button>
);
}
read more at: https://reacttraining.com/react-router/web/api/Hooks/usehistory
Simplest way in React Router 4 is to use
this.props.history.push('/new/url');
But to use this method, your existing component should have access to history object. We can get access by
If your component is linked to Route directly, then your component already has access to history object.
eg:
<Route path="/profile" component={ViewProfile}/>
Here ViewProfile has access to history.
If not connected to Route directly.
eg:
<Route path="/users" render={() => <ViewUsers/>}
Then we have to use withRouter, a heigher order fuction to warp the existing component.
Inside ViewUsers component
import { withRouter } from 'react-router-dom';
export default withRouter(ViewUsers);
That's it now, your ViewUsers component has access to history object.
UPDATE
2- in this scenario, pass all route props to your component, and then we can access this.props.history from the component even without a HOC
eg:
<Route path="/users" render={props => <ViewUsers {...props} />}
This is how I did it:
import React, {Component} from 'react';
export default class Link extends Component {
constructor(props) {
super(props);
this.onLogout = this.onLogout.bind(this);
}
onLogout() {
this.props.history.push('/');
}
render() {
return (
<div>
<h1>Your Links</h1>
<button onClick={this.onLogout}>Logout</button>
</div>
);
}
}
Use this.props.history.push('/cart'); to redirect to cart page it will be saved in history object.
Enjoy, Michael.
According to React Router v4 documentation - Redux Deep Integration session
Deep integration is needed to:
"be able to navigate by dispatching actions"
However, they recommend this approach as an alternative to the "deep integration":
"Rather than dispatching actions to navigate you can pass the history object provided to route components to your actions and navigate with it there."
So you can wrap your component with the withRouter high order component:
export default withRouter(connect(null, { actionCreatorName })(ReactComponent));
which will pass the history API to props. So you can call the action creator passing the history as a param. For example, inside your ReactComponent:
onClick={() => {
this.props.actionCreatorName(
this.props.history,
otherParams
);
}}
Then, inside your actions/index.js:
export function actionCreatorName(history, param) {
return dispatch => {
dispatch({
type: SOME_ACTION,
payload: param.data
});
history.push("/path");
};
}
Nasty question, took me quite a lot of time, but eventually, I solved it this way:
Wrap your container with withRouter and pass history to your action in mapDispatchToProps function. In action use history.push('/url') to navigate.
Action:
export function saveData(history, data) {
fetch.post('/save', data)
.then((response) => {
...
history.push('/url');
})
};
Container:
import { withRouter } from 'react-router-dom';
...
const mapDispatchToProps = (dispatch, ownProps) => {
return {
save: (data) => dispatch(saveData(ownProps.history, data))}
};
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Container));
This is valid for React Router v4.x.
I offer one more solution in case it is worthful for someone else.
I have a history.js file where I have the following:
import createHistory from 'history/createBrowserHistory'
const history = createHistory()
history.pushLater = (...args) => setImmediate(() => history.push(...args))
export default history
Next, on my Root where I define my router I use the following:
import history from '../history'
import { Provider } from 'react-redux'
import { Router, Route, Switch } from 'react-router-dom'
export default class Root extends React.Component {
render() {
return (
<Provider store={store}>
<Router history={history}>
<Switch>
...
</Switch>
</Router>
</Provider>
)
}
}
Finally, on my actions.js I import History and make use of pushLater
import history from './history'
export const login = createAction(
...
history.pushLater({ pathname: PATH_REDIRECT_LOGIN })
...)
This way, I can push to new actions after API calls.
Hope it helps!
this.context.history.push will not work.
I managed to get push working like this:
static contextTypes = {
router: PropTypes.object
}
handleSubmit(e) {
e.preventDefault();
if (this.props.auth.success) {
this.context.router.history.push("/some/Path")
}
}
Be careful that don't use react-router#5.2.0 or react-router-dom#5.2.0 with history#5.0.0. URL will update after history.push or any other push to history instructions but navigation is not working with react-router. use npm install history#4.10.1 to change the history version. see React router not working after upgrading to v 5.
I think this problem is happening when push to history happened. for example using <NavLink to="/apps"> facing a problem in NavLink.js that consume <RouterContext.Consumer>. context.location is changing to an object with action and location properties when the push to history occurs. So currentLocation.pathname is null to match the path.
In this case you're passing props to your thunk. So you can simply call
props.history.push('/cart')
If this isn't the case you can still pass history from your component
export function addProduct(data, history) {
return dispatch => {
axios.post('/url', data).then((response) => {
dispatch({ type: types.AUTH_USER })
history.push('/cart')
})
}
}
I struggled with the same topic.
I'm using react-router-dom 5, Redux 4 and BrowserRouter.
I prefer function based components and hooks.
You define your component like this
import { useHistory } from "react-router-dom";
import { useDispatch } from "react-redux";
const Component = () => {
...
const history = useHistory();
dispatch(myActionCreator(otherValues, history));
};
And your action creator is following
const myActionCreator = (otherValues, history) => async (dispatch) => {
...
history.push("/path");
}
You can of course have simpler action creator if async is not needed
Here's my hack (this is my root-level file, with a little redux mixed in there - though I'm not using react-router-redux):
const store = configureStore()
const customHistory = createBrowserHistory({
basename: config.urlBasename || ''
})
ReactDOM.render(
<Provider store={store}>
<Router history={customHistory}>
<Route component={({history}) => {
window.appHistory = history
return (
<App />
)
}}/>
</Router>
</Provider>,
document.getElementById('root')
)
I can then use window.appHistory.push() anywhere I want (for example, in my redux store functions/thunks/sagas, etc) I had hoped I could just use window.customHistory.push() but for some reason react-router never seemed to update even though the url changed. But this way I have the EXACT instance react-router uses. I don't love putting stuff in the global scope, and this is one of the few things I'd do that with. But it's better than any other alternative I've seen IMO.
If you are using Redux, then I would recommend using npm package react-router-redux. It allows you to dispatch Redux store navigation actions.
You have to create store as described in their Readme file.
The easiest use case:
import { push } from 'react-router-redux'
this.props.dispatch(push('/second page'));
Second use case with Container/Component:
Container:
import { connect } from 'react-redux';
import { push } from 'react-router-redux';
import Form from '../components/Form';
const mapDispatchToProps = dispatch => ({
changeUrl: url => dispatch(push(url)),
});
export default connect(null, mapDispatchToProps)(Form);
Component:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export default class Form extends Component {
handleClick = () => {
this.props.changeUrl('/secondPage');
};
render() {
return (
<div>
<button onClick={this.handleClick}/>
</div>Readme file
);
}
}
I was able to accomplish this by using bind(). I wanted to click a button in index.jsx, post some data to the server, evaluate the response, and redirect to success.jsx. Here's how I worked that out...
index.jsx:
import React, { Component } from "react"
import { postData } from "../../scripts/request"
class Main extends Component {
constructor(props) {
super(props)
this.handleClick = this.handleClick.bind(this)
this.postData = postData.bind(this)
}
handleClick() {
const data = {
"first_name": "Test",
"last_name": "Guy",
"email": "test#test.com"
}
this.postData("person", data)
}
render() {
return (
<div className="Main">
<button onClick={this.handleClick}>Test Post</button>
</div>
)
}
}
export default Main
request.js:
import { post } from "./fetch"
export const postData = function(url, data) {
// post is a fetch() in another script...
post(url, data)
.then((result) => {
if (result.status === "ok") {
this.props.history.push("/success")
}
})
}
success.jsx:
import React from "react"
const Success = () => {
return (
<div className="Success">
Hey cool, got it.
</div>
)
}
export default Success
So by binding this to postData in index.jsx, I was able to access this.props.history in request.js... then I can reuse this function in different components, just have to make sure I remember to include this.postData = postData.bind(this) in the constructor().
so the way I do it is:
- instead of redirecting using history.push, I just use Redirect component from react-router-dom
When using this component you can just pass push=true, and it will take care of the rest
import * as React from 'react';
import { Redirect } from 'react-router-dom';
class Example extends React.Component {
componentDidMount() {
this.setState({
redirectTo: '/test/path'
});
}
render() {
const { redirectTo } = this.state;
return <Redirect to={{pathname: redirectTo}} push={true}/>
}
}
Use Callback. It worked for me!
export function addProduct(props, callback) {
return dispatch =>
axios.post(`${ROOT_URL}/cart`, props, config)
.then(response => {
dispatch({ type: types.AUTH_USER });
localStorage.setItem('token', response.data.token);
callback();
});
}
In component, you just have to add the callback
this.props.addProduct(props, () => this.props.history.push('/cart'))
React router V4 now allows the history prop to be used as below:
this.props.history.push("/dummy",value)
The value then can be accessed wherever the location prop is available as
state:{value} not component state.
As we have a history already included in react router 5, we can access the same with reference
import React from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
function App() {
const routerRef = React.useRef();
const onProductNav = () => {
const history = routerRef.current.history;
history.push("product");
}
return (
<BrowserRouter ref={routerRef}>
<Switch>
<Route path="/product">
<ProductComponent />
</Route>
<Route path="/">
<HomeComponent />
</Route>
</Switch>
</BrowserRouter>
)
}
step one wrap your app in Router
import { BrowserRouter as Router } from "react-router-dom";
ReactDOM.render(<Router><App /></Router>, document.getElementById('root'));
Now my entire App will have access to BrowserRouter. Step two I import Route and then pass down those props. Probably in one of your main files.
import { Route } from "react-router-dom";
//lots of code here
//somewhere in my render function
<Route
exact
path="/" //put what your file path is here
render={props => (
<div>
<NameOfComponent
{...props} //this will pass down your match, history, location objects
/>
</div>
)}
/>
Now if I run console.log(this.props) in my component js file that I should get something that looks like this
{match: {…}, location: {…}, history: {…}, //other stuff }
Step 2 I can access the history object to change my location
//lots of code here relating to my whatever request I just ran delete, put so on
this.props.history.push("/") // then put in whatever url you want to go to
Also I'm just a coding bootcamp student, so I'm no expert, but I know you can also you use
window.location = "/" //wherever you want to go
Correct me if I'm wrong, but when I tested that out it reloaded the entire page which I thought defeated the entire point of using React.
Create a custom Router with its own browserHistory:
import React from 'react';
import { Router } from 'react-router-dom';
import { createBrowserHistory } from 'history';
export const history = createBrowserHistory();
const ExtBrowserRouter = ({children}) => (
<Router history={history} >
{ children }
</Router>
);
export default ExtBrowserRouter
Next, on your Root where you define your Router, use the following:
import React from 'react';
import { /*BrowserRouter,*/ Route, Switch, Redirect } from 'react-router-dom';
//Use 'ExtBrowserRouter' instead of 'BrowserRouter'
import ExtBrowserRouter from './ExtBrowserRouter';
...
export default class Root extends React.Component {
render() {
return (
<Provider store={store}>
<ExtBrowserRouter>
<Switch>
...
<Route path="/login" component={Login} />
...
</Switch>
</ExtBrowserRouter>
</Provider>
)
}
}
Finally, import history where you need it and use it:
import { history } from '../routers/ExtBrowserRouter';
...
export function logout(){
clearTokens();
history.push('/login'); //WORKS AS EXPECTED!
return Promise.reject('Refresh token has expired');
}
you can use it like this as i do it for login and manny different things
class Login extends Component {
constructor(props){
super(props);
this.login=this.login.bind(this)
}
login(){
this.props.history.push('/dashboard');
}
render() {
return (
<div>
<button onClick={this.login}>login</login>
</div>
)
/*Step 1*/
myFunction(){ this.props.history.push("/home"); }
/**/
<button onClick={()=>this.myFunction()} className={'btn btn-primary'}>Go
Home</button>
If you want to use history while passing a function as a value to a Component's prop, with react-router 4 you can simply destructure the history prop in the render attribute of the <Route/> Component and then use history.push()
<Route path='/create' render={({history}) => (
<YourComponent
YourProp={() => {
this.YourClassMethod()
history.push('/')
}}>
</YourComponent>
)} />
Note: For this to work you should wrap React Router's BrowserRouter Component around your root component (eg. which might be in index.js)