Alternative of the old CreateBrowserHistory() function in React Router v6 - reactjs

I want to push a new route in history of my app, and as you know, the UseHistory() hook and CreateBrowserHistory() function (to create a user-defined history) have been removed from new version of 6. I know that I can use UseNavigate hook and do this work with Navigate() function, but this hook cannot be called at the top level and must be called in a React function component. Codes below show how we could do this with React Router version 5.1:
Passing the history to the parent component and exporting the same history object for using it anywhere in Application:
import React from 'react'
import { Router } from 'react-router-dom'
import { createBrowserHistory } from 'history' // which comes along with React-Router
export const history = createBrowserHistory();
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render(
<Router history={history}>
<App />
</Router>
)
Using the exported history object in another place of Application at the top level:
switch (status) {
case 404:
history.push('/not-found');
break
if I want to do this at the top level in the new version of 6, What do you recommend?

Related

Proper way of accessing the `history` object from redux thunks?

I have some redux thunks that need to call history.push().
It first I was trying to avoid having to pass the history object in the thunk call, so I did the following change in my code:
From this:
index.tsx
import { BrowserRouter } from "react-router-dom";
ReactDOM.render(
<BrowserRouter>
<App/>
</BrowserRouter>
);
To this:
index.tsx
import { Router } from "react-router-dom";
import { history } from "./history";
ReactDOM.render(
<Router history={history}>
<App/>
</Router>
);
history.ts
import { createBrowserHistory } from "history";
export const history = createBrowserHistory();
But now that I've come to the SSR phase of my project, this has become a nightmare. Because I cannot call createBrowserHistory() on my server code (NodeJS).
QUESTION
The pattern above is very comfortable to use both with redux thunks and some other helper functions that need to call history.push(). I simpy have to import { history } from "#src/history"; from whatever file that needs it and use it directly.
But since this is conflicting with my SSR code, I'd like to know what is the best practice / recommended approach to this situation. Should I get the history object from the components by using const history = useHistory() and pass it to the thunks / functions that need it? Or is there a better approach?

Navigating Using React Router From a Button Outside the React App

I have a simple demo react app that uses react-router-dom (5.2) to show one of 3 "pages".
The app is included on a page that has a button:
index.html:
<button data-app-button data-sku='woo-beanie'>Click Me</button>
index.js:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
document.addEventListener('click', function (event) {
if (event.target.closest('button[data-app-button]')) {
// send instructions to react
}
});
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
I want to be able to navigate to a page in the react site, passing through the buttons data-attributes. How is this done with react and react-router ?
UPDATE
#Doppoio's solution works - as long as I'm on a different "page" in my react app. However I have a route like this:
<Route
path="/tryon/:id/:product_sku?">
</Route>
If I start in app from a route of say /faqs and my external button navigates to /tryon/242/jumper-23 my component is awar of the product_sku property.
However when I'm on a page in app of /tryon/242 and then i click an external button to navigate to /tryon/242/jumper-23 the component should be aware of the jumper-23 optional parameter. Currently it isn't.
How do i make the Tryon component detect the change in url of just the optional parameter?
Somewhere in your code under Router, you can add history to window object. And call it from there.
const SomeComponentInsideRouter = () => {
const history = useHistory();
useEffect(() => {
window.reactHistory = history; // Add reference to history via window object.
}, []);
return null;
};
And call it via window.reactHistory
document.addEventListener("click", function (event) {
if (event.target.closest("button[data-app-button]")) {
// send instructions to react
window.reactHistory.push("/about");
}
});
Here's sandbox link
https://codesandbox.io/s/happy-ganguly-b0u2o?file=/src/index.js
Update to mention changed props:
Changes to the props can be detected using componentDidUpdate
https://reactjs.org/docs/react-component.html#componentdidupdate

Cannot read property 'history' of undefined (useHistory hook of React Router 5)

I am using the new useHistory hook of React Router, which came out a few weeks ago. My React-router version is 5.1.2. My React is at version 16.10.1. You can find my code at the bottom.
Yet when I import the new useHistory from react-router, I get this error:
Uncaught TypeError: Cannot read property 'history' of undefined
which is caused by this line in React-router
function useHistory() {
if (process.env.NODE_ENV !== "production") {
!(typeof useContext === "function") ? process.env.NODE_ENV !== "production" ? invariant(false, "You must use React >= 16.8 in order to use useHistory()") : invariant(false) : void 0;
}
return useContext(context).history; <---------------- ERROR IS ON THIS LINE !!!!!!!!!!!!!!!!!
}
Since it is related to useContext and perhaps a conflict with context is at fault, I tried completely removing all calls to useContext, creating the provider, etc. However, that did nothing. Tried with React v16.8; same thing.
I have no idea what could be causing this, as every other feature of React router works fine.
***Note that the same thing happens when calling the other React router hooks, such as useLocation or useParams.
Has anyone else encountered this? Any ideas to what may cause this?
Any help would be greatly appreciated, as I found nothing on the web related to this issue.
import React, {useEffect, useContext} from 'react';
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
import { Switch, useHistory } from 'react-router'
import { useTranslation } from 'react-i18next';
import lazyLoader from 'CommonApp/components/misc/lazyLoader';
import {AppContext} from 'CommonApp/context/context';
export default function App(props) {
const { i18n } = useTranslation();
const { language } = useContext(AppContext);
let history = useHistory();
useEffect(() => {
i18n.changeLanguage(language);
}, []);
return(
<Router>
<Route path="/">
<div className={testClass}>HEADER</div>
</Route>
</Router>
)
}
It's because the react-router context isn't set in that component. Since its the <Router> component that sets the context you could use useHistory in a sub-component, but not in that one.
Here is a very basic strategy for solving this issue:
const AppWrapper = () => {
return (
<Router> // Set context
<App /> // Now App has access to context
</Router>
)
}
const App = () => {
let history = useHistory(); // Works!
...
// Render routes in this component
Then just be sure to use the "wrapper" component instead of App directly.
Note to other people that run into this problem and already have wrapped the component with Router component. Make sure that Router and the useHistory hook are imported from the same package. The same error can be thrown when one of them are imported from react-router and the other one from react-router-dom and the package versions of those packages don't match. Don't use both of them, read about the difference here.
useHistory won't work in the component where you have your Routes because the context which is needed for useHistory is not yet set.
useHistory will work on any child component or components which you have declared in your Router but it won't work on Router's parent component or Router component itself.
The solution is:
in the Main (father) component
import { BrowserRouter } from "react-router-dom";
<BrowserRouter><App /></BrowserRouter>
in the child component (App)
import { withRouter } from "react-router-dom";
function App(props) {
const { i18n } = useTranslation();
const { language } = useContext(AppContext);
let history = useHistory();
useEffect(() => {
i18n.changeLanguage(language);
}, []);
return(
<Route path="/">
<div className={testClass}>HEADER</div>
</Route>
)
}
export default withRouter(App);
I updated my react-router-dom from 5.0.0 to ^5.1.2 and it's been solved. You may notice that the useHistory is in a sub-component.
Use BrowserRouter.
import {
BrowserRouter as Router,
Route,
Switch,
} from 'react-router-dom';
If you use Router, then you need to specify a history for it:
import {
Router,
Route,
Switch,
} from 'react-router-dom';
// Ensure you destructure the createBrowserHistory object
import { createBrowserHistory } from 'history';
const history = createBrowserHistory();
return (
<Router history={history} >
...
</Router>
);
In a short, you should move const history = useHistory(); to your sub-component

How to access react-router-v4 history prop from MobX store

Is there any way to create an action in your MobX store, which pushes your app to a new url using react router v4, e.g. this.props.history.push...
I constantly get a history undefined error, but am unsure how to access the history from my store.
The history push called from the component itself does work though..
many thanks! (this is driving me crazy..)
Since I stumbled across the same issue, I'll share my solution. I just put the RouterStore into its own file in my stores directory, then if I needed access to history or location or whatever, I would import the routing store into the store I was currently working in.
./stores/routing.ts
import { RouterStore } from 'mobx-react-router'
export default new RouterStore()
./stores/other-store.ts
import routing from './routing'
export class OtherStore {
#action
doSomething = () => {
routing.push('/new-route')
}
}
export default new OtherStore()
./index.ts
import { Router } from 'react-router-dom'
import { Provider } from 'mobx-react'
import createBrowserHistory from 'history/createBrowserHistory'
import { syncHistoryWithStore } from 'mobx-react-router'
import otherStore from './stores/other-store'
import routing from './stores/routing'
const browserHistory = createBrowserHistory()
const stores = {
otherStore,
routing,
}
const history = syncHistoryWithStore(browserHistory, routing)
ReactDOM.render(
<Provider {...stores}>
<Router history={history}>
<App />
</Router>
</Provider>,
document.getElementById('root'),
)
You can use mobx-react-router to put react-router in a mobx store and then use it by injecting it in components.
You can also pass the router store as a constructor argument to your other stores that need it. This way you have the router history instance available in your mobx store.
I would like to add a simpler solution that does not require any additional libraries. React Router version is 5.2
Among my stores i've created a HistoryStore.js with the following code:
import { createBrowserHistory } from 'history';
export class HistoryStore {
history = createBrowserHistory();
}
Then I create an instance of it in my contexts.js file but you could do it right away.
export const history = new HistoryStore();
After that you import it in your index.js and pass it as a history prop to the Router.
That's it. Now you could import this store into any other and use it there. When you use useHistory hook in your component it gets this history object, so your history in synchronized.

React router 4 history.listen never fires

Switched to router v4 and history v4.5.1 and now history listener not working
import createBrowserHistory from 'history/createBrowserHistory'
const history = createBrowserHistory()
history.listen((location, action) => {
console.log(action, location.pathname, location.state) // <=== Never happens
})
render(
<Provider store={store}>
<Router history={history}>
...
</Router>
</Provider>,
document.getElementById('root')
)
Any ideas why it is being ignored?
Since you are using BrowserRouter(with import alias Router as mentioned in comments of the question), it doesn't care the history prop you pass in. Instead of that it internally creates and assigns new browser history to the Router. So the history instance that you listen and being used in Router is not the same. That's why your listener doesn't work.
Import the original Router.
import { Router } from 'react-router-dom';
It will work as you expect.
The problem is that you are creating your own history object and passing it into the router. However, React Router v4 already provides this object for you, via this.props. (Importing Router has nothing to do with this)
componentDidMount() {
this.props.history.listen((location, action) => console.log('History changed!', location, action));
}
You may need to layer your app a bit more though, like below, and put this componentDidMount method in your MyApp.jsx and not directly at the very top level.
<Provider store={store}>
<BrowserRouter>
<MyApp/>
</BrowserRouter>
</Provider>
(Or use NativeRouter instead of BrowserRouter if you're doing React Native)
works like a magic: 🧙‍♂️🧙‍♂️🧙‍♂️
const history = useHistory();
useEffect(() => {
if (history) { // for jest sake ))
history.listen(() => {
store.reset();
});
}
}, []);

Resources