Lots of applications need more control over their navigation than what their router provides
No worries, we are here to help
Compatible with both react-router v6 and v5 API
📜 Save all navigation history in store Get started
🌲 Persist history after reloading the page Read more
⏭️ Skipping screens capability out of the box Read more
🔀 Dispatch location changes Read more
👊 Force current route to re-render Read more
🚦 Selectors for easy access Read more
🐛 Easy debug, find everything you need to know about navigation in your favorite dev tools:
Let's get started by installing the package
pnpm add react-redux-history
npm i react-redux-history
yarn add react-redux-history
Create a browser router and pass it to configureRouterHistory
. The returned reducer and middleware will be used to connect to the store
// store.js
import { configureRouterHistory } from 'react-redux-history'
import { createBrowserRouter } from 'react-router-dom'
import { routes } from 'src/routes'
// optional, defaults are listed below
const options = {
storageKey: 'routerState',
storageLimit: Infinity
}
export const router = createBrowserRouter(routes);
export const { routerReducer, routerMiddleware } = configureRouterHistory({
router,
...options
})
Backwards compatibility with react-router
legacy v5 API is also supported
// store.js
import { configureRouterHistory } from 'react-redux-history'
import { createBrowserHistory } from 'history'
const options = { ... }
export const history = createBrowserHistory() // react-router v5 API
export const { routerReducer, routerMiddleware } = configureRouterHistory({
history,
...options
})
For more info regarding differences between v5 and v6 API check out the official docs
Add the reducer and middleware to your store. It should look something like this
// store.js
const store = configureStore({
reducer: combineReducers({
// ...other reducers
router: routerReducer
}),
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware()
// ...other middleware
.concat(routerMiddleware)
})
export default store
Lastly, add either <LocationListener />
or useLocationListener
at app's root
// App.tsx
import { useLocationListener, LocationListener } from 'react-redux-history'
import { router } from 'src/store' // use `history` if working with v5 API
const App = () => {
useLocationListener(router) // use either this or the component below, not both!
return (
<>
...
<LocationListener router={router} />
...
</>
)
}
Note: the router
/ history
objects provided to configureRouterHistory
and useLocationListener
/ LocationListener
must be the same objects !
The middleware can be configured by passing an options object to configureRouterHistory
.
The following options are available:
storageKey
- the key to use when saving the state to session storage. Defaults torouterState
storageLimit
- the maximum number of entries to save in session storage. Defaults toInfinity
Be careful when limiting session storage entries. The user is still able to go back to previous pages even if they are not saved in session storage. This can cause unexpected behaviour on page reload, especially if you use skipBack
/ skipForward
or similar logic that alters the navigation flow.
History is persisted after page refresh by leveraging session storage.
This helps provide a better user experience and allows you to build a more robust navigation system.
By setting a skipBack
/ skipForward
flag on a specific route the user will be automatically skipped over certain routes.
history.push({
pathname: 'page_5',
state: { skipBack: 4 }
})
In this example, every time the user will try to go back from page_5 he will be skipped back 4 pages, reaching page_1. The same behaviour will apply when going forward from page_1, the user will be skipped forward to page_5.
Note: Due to the restrictive nature of browser navigation, back or forward actions cannot be stopped. That means that in the previous example the user will actually reach page_4 before being redirected to page_1. If there is conflicting logic (such as extra redirects) in page_4, it will be fired before the middleware manages to completely skip all screens. In order to get past this issue we can selectIsSkipping
to not render the component tree while skipping.
We managed this at Utilmond by leveraging the selector in our general purpose loading component. When the flag is true
, we render a loading backdrop instead of the current route. This prevents any conflicting logic to be fired and mess with the redirects.
Change current location using redux actions anywhere in your app.
The API is compatible with history
, it can be used as a drop-in replacement.
import { push, replace, forward, back, go } from 'react-redux-history'
dispatch(push({
pathname: 'homepage',
state: {
...
}
}))
// or use the short version
dispatch(push('homepage'))
Force current route to re-render by using selectForceRender
. Navigate to the same route while passing forceRender: {}
in location state.
import { useSelector } from 'react-redux'
import { selectForceRender } from 'react-redux-history'
const Component = () => {
// The component will re-render every time the `forceRender` flag reference changes
const forceRender = useSelector(selectForceRender)
useEffect(() => {
// The flag can also be used as a dependency in order to re-trigger effects
}, [forceRender])
return (
<button
onClick={() => {
history.push({
// By default `react-router` will not trigger re-rendering when the pathname is the same
pathname: 'current_pathname',
state: {
// Simply pass a new object to force re-rendering
forceRender: {}
},
})
}}
>
Re-render
</button>
)
}
There are also a few useful selectors for easy access:
selectLocationHistory
selectCurrentLocation
selectCurrentLocationState
selectCurrentLocationIndex
selectNextLocation
selectPreviousLocation
selectBackLocation
selectHistoryAction
selectIsSkippingRoutes
selectIsNewSession
selectForceRender