Is possible to override History.push method of React-Router-Dom - reactjs

I want to attach a computed value of query string into URL whenever user navigates with history.push method.
here is my code
let pusher: null | ((a: unknown, b?: unknown) => void) = null;
/**
* This hook will push a hashed value of query string of the url
*
* Don't mind about memory leak, it's `Singletom`
* #param history an instance of History from useHistory
* #returns `Pusher` an enhance version of `History.push`
*/
export default function useHashPusher(history: ReturnType<typeof useHistory>){
const sha256 = useSha256();
if(!pusher) {
pusher = function(...args) {
const data = args[0];
if(typeof data === 'object' && data && 'search' in data){
const raw = data['search' as keyof typeof data];
if(raw){
const hashed = sha256.hash(raw);
(data['search' as keyof typeof data] as string) = `${raw}&hash=${hashed}`;
console.log(data);
}
}
console.log('overrided!', args[0]);
history.push(...args);
};
}
return pusher;
}
It works OK but I have to update code at many place from history.push to my pusher
Is there any way to override the history.push method? I'm using react-router-dom v5

It looks like my commented suggestion does work. In a separate file create and export a custom history object.
Example:
import { createBrowserHistory, History } from "history";
import { sha256 } from "../path/to/sha256"; // *
const historyBase = createBrowserHistory();
const history: History = {
...historyBase,
push: (...args) => {
const data = args[0];
if (typeof data === "object" && data && "search" in data) {
const raw = data["search" as keyof typeof data];
if (raw) {
const hashed = sha256.hash(raw); // *
(data[
"search" as keyof typeof data
] as string) = `${raw}&hash=${hashed}`;
console.log(data);
}
}
console.log("overrided!", args[0]);
historyBase.push(...args);
}
};
export default history;
* Note: The useSha256 hook won't work here for this, but since I wasn't able to find any NPM package that provided this hook I am assuming this is a local custom hook. If this is the case then import the object the hook was previously.
Import the low-level base Router component from react-router-dom and your custom history object and pass history as a prop.
Example:
import { StrictMode } from "react";
import * as ReactDOMClient from "react-dom/client";
import { Router } from "react-router-dom";
import history from './history';
import App from "./App";
const rootElement = document.getElementById("root");
const root = ReactDOMClient.createRoot(rootElement);
root.render(
<StrictMode>
<Router history={history}>
<App />
</Router>
</StrictMode>
);
At this point the router is using your history object in it's context, so all navigation actions will use it. This means not only directly accessing history in nested children, but Link and other components that take a To object argument as well.
Example:
import { Link, useHistory } from "react-router-dom";
export default function App() {
const history = useHistory();
const handler = () => {
history.push({
pathname: "/test",
search: "?....."
});
};
return (
<div className="App">
...
<ul>
...
<li>
<Link to={{ pathname: "/foo", search: "?....." }}>
Foo
</Link>
</li>
</ul>
<button type="button" onClick={handler}>
Navigate
</button>
</div>
);
}

Related

React Router Dom v6.4 doesn't allow history.listen (prior suggestions deprecated) with createBrowserRouter or createMemoryRouter

react-router-dom v. 6.4.2 doesn't allow history.listen as referenced in the code example below. This is for a mfe with module federation.
In the code example using history.listen, if a link is clicked in the remote (loaded as mfe) then the memory history (memory router now) current path will be updated. It will then call onNavigate to tell the host container which is using browser history (browser router now) that the current path has changed.
Prior suggestions were to use UNSAFE_NavigationContext, useHistory, unstable_HistoryRouter, import {...} from 'history', etc. Apparently, those prior methods were temporary migration aids from v5 to v6.3 and with v6.4+ are now deprecated in favor of the new data api's in 6.4. See here
we do not intend to support custom histories moving forward. This API
is here as a migration aid. We recommend removing custom histories
from your app.
Additionally, from the maintainers of RRD:
We recommend updating your app to use one of the new routers from 6.4.
After searching here and within both open and closed issues on remix-RRD I have been unable to find a workable solution based on the above for replacing history.listen, .push or .location with the new data api's (routers) using createBrowserRouter or createMemoryRouter as referenced here
There are many open issues on the react-router-dom page relating to this use case.
Original marketing/src/bootstrap.tsx from remote
import React from 'react'
import { createRoot } from 'react-dom/client'
import { createMemoryHistory, createBrowserHistory } from 'history' <= Not Supported
import App from './App'
let root: { render: (arg0: JSX.Element) => void } | null = null
// Mount function to start up the app
const mount = (el: any, { onNavigate, defaultHistory, initialPath }: any) => {
if (!el) {
root = null
return
}
// If defaultHistory in development and isolation use BrowserHistory
const history =
defaultHistory ||
// Otherwise use MemoryHistory and initial path from container
createMemoryHistory({
initialEntries: [initialPath],
})
if (onNavigate) {
history.listen(onNavigate) <= Not Supported
}
root = root ? root : createRoot(el)
root.render(<App history={history} />)
return {
onParentNavigate({ pathname: nextPathname }: any) {
const { pathname } = history.location <= Not Supported
if (pathname !== nextPathname) {
history.push(nextPathname) <= Not Supported
}
},
}
}
// If we are in development and in isolation,
// call mount immediately
if (process.env.NODE_ENV === 'development') {
const devRoot = document.querySelector('#_marketing-dev-root')
if (devRoot) {
mount(devRoot, { defaultHistory: createBrowserHistory() })
}
}
// We are running through container
// and we should export the mount function
export { mount }
Replacement marketing/src/bootstrap.tsx from remote (in progress)
import React from 'react'
import { createRoot } from 'react-dom/client'
import {
createBrowserRouter,
createMemoryRouter,
} from 'react-router-dom'
import App from './App'
import ErrorPage from './pages/ErrorPage'
import Landing from './components/Landing'
import Pricing from './components/Pricing'
let root: { render: (arg0: JSX.Element) => void } | null = null
const routes = [
{
path: '/',
errorElement: <ErrorPage />,
children: [
{
index: true,
element: <Landing />,
errorElement: <ErrorPage />,
},
{
path: 'pricing',
element: <Pricing />,
errorElement: <ErrorPage />,
},
],
},
]
// Mount function to start up the app
const mount = (
el: Element,
{
onNavigate,
defaultRouter,
}: {
onNavigate: (() => void) | null
defaultRouter: any
},
): unknown => {
if (!el) {
root = null
return
}
// if in development and isolation, use browser router. If not, use memory router
const router = defaultRouter || createMemoryRouter(routes)
if (onNavigate) {
router.listen(onNavigate) // There is no history.listen anymore. router.listen is not a function
}
root = root ? root : createRoot(el)
root.render(<App router={router} />)
}
// If we are in development and in isolation,
// call mount immediately
if (process.env.NODE_ENV === 'development') {
const devRoot = document.querySelector('#_marketing-dev-root')
if (devRoot) {
mount(devRoot, { defaultRouter: createBrowserRouter(routes) })
console.log('defaultRouter')
}
}
// We are running through container
// and we should export the mount function
export { mount }
Original marketing/src/App.tsx from remote
import './MuiClassNameSetup'
import React from 'react'
import { Switch, Route, Router } from 'react-router-dom'
import Landing from './components/Landing'
import Pricing from './components/Pricing'
export default function _({ history }: any) {
return (
<div>
<Router history={history}>
<Switch>
<Route exact path="/pricing" component={Pricing} />
<Route path="/" component={Landing} />
</Switch>
</Router>
</div>
)
}
Replacement marketing/src/App.tsx from remote (in progress)
import './MuiClassNameSetup'
import React from 'react'
import {
RouterProvider,
} from 'react-router-dom'
export default function App({ router }: any) {
return <RouterProvider router={router} />
}
Original container/src/components/MarketingApp.tsx from host
import { mount } from 'marketing/MarketingApp'
import React, { useRef, useEffect } from 'react'
import { useHistory } from 'react-router-dom' <= Not Supported
export default function _() {
const ref = useRef(null)
const history = useHistory() <= Not Supported
useEffect(() => {
const { onParentNavigate } = mount(ref.current, {
initialPath: history.location.pathname,
onNavigate: ({ pathname: nextPathname }: any) => {
const { pathname } = history.location <= Not Supported
if (pathname !== nextPathname) {
history.push(nextPathname) <= Not Supported
}
},
})
history.listen(onParentNavigate) <= Not Supported
}, [history])
return <div ref={ref} />
}
Replacement container/src/components/MarketingApp.tsx from host (in progress)
import { mount } from 'marketing/MarketingApp'
import React, { useRef, useEffect } from 'react'
export default function _() {
const ref = useRef(null)
useEffect(() => {
mount(ref.current, {
onNavigate: () => {
console.log('The container noticed navigation in Marketing')
},
})
})
return <div ref={ref} />
}
Looking for a solution to replace history.listen, history.location and history.push that works with the new v6.4 data api's?
One of the maintainers of RRD just posted a new implementation detail to replace history.listen which is for v6.4+. See router.subscribe() below.
let router = createBrowserRouter(...);
// If you need to navigate externally, instead of history.push you can do:
router.navigate('/path');
// And instead of history.replace you can do:
router.navigate('/path', { replace: true });
// And instead of history.listen you can:
router.subscribe((state) => console.log('new state', state));
Unfortunately, the new implementation is also unstable and is considered a beta test implementation.
Now, for the bad news 😕 . Just like unstable_HistoryRouter we also
consider this type of external navigation and subscribing to be
unstable, which is why we haven't documented this and why we've marked
all the router APIs as #internal PRIVATE - DO NOT USE in
JSDoc/Typescript. This isn't to say that they'll forever be unstable,
but since it's not the normally expected usage of the router, we're
still making sure that this type of external-navigation doesn't
introduce problems (and we're fairly confident it doesn't with the
introduction of useSyncExternalStore in react 18!)
If this type of navigation is necessary for your app and you need a
replacement for unstable_HistoryRouter when using RouterProvider then
we encourage you use the router.navigate and router.subscribe methods
and help us beta test the approach! Please feel free to open new GH
issues if you run into any using that approach and we'll use them to
help us make the call on moving that towards future stable release.

cant access the state in a functional component with useLocation from react-router-dom

I'm trying to access a state property I'm passing through NavLink into a component but I can't get it to work. I read that it was as simple as using useLocation to access the state, but I think typescript does not like this.
below code:
import React from "react";
import "./project.css";
import Toolbar from "../navigation/toolbar";
import Footer from "../footer/footer";
import projects from "../projectsSection/projects";
import { useLocation } from "react-router-dom";
interface ProjectProps {
projectId: number;
}
const Project: React.FC = (props) => {
const { state } = useLocation();
const { projectId } = state;
const project = projects.find((project) => project.id === projectId);
return (
<section className="project-main">
<Toolbar />
<div></div>
<Footer />
</section>
);
};
export default Project;
Any idea how can I access state in similar setup? I get an error that says: Property 'projectId' does not exist on type 'unknown
You need to add a generic type to useLocation:
const { state } = useLocation<{projectId: string}>();

How to use useParams in mapStateToProps?

I want specific prop from route params and use it to filter data in redux-store.
Product.js
import React from 'react';
import { useParams } from 'react-router-dom';
import { connect } from 'react-redux';
const Product = (props) => {
let { slug } = useParams();
//console.log(props.match)
return (
<div>
<h3>Welcome to <b>{ slug }</b> page</h3>
</div>
)
}
const mapStateToProps = ( state, ownProps ) => {
// let id = slug;
return { item: state.items[0]}
}
export default connect(
mapStateToProps
)(Product);
App.js
function App() {
return (
<Router>
<Navbar/>
<Switch>
<Route exact path="/:slug">
<Product/>
</Route>
<Route exact path="/">
<Home/>
</Route>
</Switch>
</Router>
);
}
and whatever links that navigate to /slug path are ended up in Product.js, Since Product.js is being nested nowhere else so i couldn't get specific props pass down but route params. From my perspective this is completely wrong but i couldn't figure out a better way to do it.
Since you are using the new version of React and Redux. You can try use Hook to get data from redux store.
Better call useSelector instead. Read more here
import React from "react";
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
const Product = () => {
let { slug } = useParams();
const item = useSelector((state) => state.items[slug]);
console.log(item);
return (
<div>
<h3>
Welcome to <b>{slug}</b> page
</h3>
</div>
);
};
export default Product;
In your case, you could use the mapDispatchToProps property which is the second argument of connect
Product.js
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { connect } from 'react-redux';
const Product = (props) => {
const { slug } = useParams();
const {
items, // From mapStateToProps
filterItems // From mapDispatchToProps
} = props;
const [filteredItems, setFilteredItems] = useState([]);
useEffect(() => {
setFilteredItems(filterItems(items, slug));
});
return (
<div>
<h3>Welcome to <b>{ slug }</b> page</h3>
<!-- {filteredItems.map(item => { })} -->
</div>
)
}
const mapStateToProps = ( state, ownProps ) => {
return { items: state.items}
}
const mapDispatchToProps = dispatch => {
return {
filterItems: (items, filter) => {
// TODO: Filter logic goes here...
return items;
}
}
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(Product);
Another performant solution is to use withRouter
You want to access the state and routing params in one place to select what you want from the state.
One solution to it is using useParams inside the component and while you access the state using connect it's fine.
However, I once found that this solution causes my component to re-render a lot because we don't quietly control how useParams being re-invoked, I prefer using the HOC(higher order component) that react-router-dom offer which is called withRouter (which I found more performant) and here is how to use it
You wrap it around connect
import { withRouter } from "react-router-dom";
...
export const ArticlePageContainer = withRouter(
connect(mapStateToProps, undefined)(ArticlePageUI)
);
then you can access the slug or any params from inside the props in mapStateToProps function
function mapStateToProps(state, props) {
const slug = props.match.params.slug;
return {
targetArticle: state.items.find((item) => item.slug == slug)
};
}
Finally, you use that selected piece of data as in your component, where you get it from the props directly now.
function ArticlePageUI(props) {
return (
<>
<p>{"Article Page"}</p>
<p>{props.targetArticle?.content}</p>
</>
);
}
Here's a code sandbox where you can check the implementation yourself
https://codesandbox.io/s/stackoverflowhow-to-use-useparams-in-mapstatetoprops-qxxdo?file=/src/article-page.js:87-225

NextJS. How do i get canonical URL for dynamic pages?

Before nextjs 9.4 i have been using
next-absolute-url package which gives origin in getInitialProps.
nextsJs 9.5
since with Automatic optimisation we use getServerSideProps and getStaticProps. the package doesn't support these methods and return undefined.
My Question is that if we have dynamic page lets say post/[pid].tsx using the next-seo i require canonical url for it how could i generate the with getStaticProps or getServerSideProps.
There is another question asked with no response here
How to access canonical URL in Next.js with Automatic Static Optimization turned on?
So After Update (Removing Redux from Application) the next-absoulute-url does work on getServerSideProps.
If application state is not too complex React-context API with Hooks work greats. have a read here
https://kentcdodds.com/blog/application-state-management-with-react
this works fine for me
//_app.tsx
import type { AppContext, AppInitialProps } from 'next/app';
import App from 'next/app';
import { appWithTranslation } from 'next-i18next';
import { store } from '##/redux/store';
import { Provider } from 'react-redux';
import Continuous from '#/components/_pages/Continuous';
import GlobalHead from '#/components/_pages/globals/Head';
import Layout from '#/components/Layout';
type IProps = { canonical: string}
class MyApp extends App<IProps> {
static async getInitialProps({ Component, ctx, }: AppContext): Promise<AppInitialProps<any> & IProps> {
const { locale, defaultLocale, asPath } = ctx;
const _locale = locale === defaultLocale ? '' : `/${locale}`;
const _path = asPath === '/' ? '' : asPath;
const host = 'https://focus-hours.com'
return {
pageProps: Component.getInitialProps ? await Component.getInitialProps(ctx) : {},
canonical: `${host}${_locale}${_path}`,
};
}
render(): JSX.Element {
const { Component, pageProps, canonical } = this.props;
return (
<>
<GlobalHead />
<Head>
<link rel="canonical" href={canonical} />
</Head>
<Provider store={store}>
<Layout>
<Component {...pageProps} />
</Layout>
{_isClient && <Continuous />}
</Provider>
</>
)
}
}
export default appWithTranslation<any>(MyApp);
If you need to get the pid from the url, you need to use router.query in the dynamic route page,
which will give you an object like
{"pid":123}
// post/[pid].tsx
import { useRouter } from 'next/router'
getStaticProps(){
const router = useRouter();
const pid = router.query.pid;
return {props:{pid}}
}
I found the perfect documentation for this in this url - https://nextjs.org/docs/routing/dynamic-routes

passing parameters through this.props.history.push('/xyz) in react router [duplicate]

How can we pass parameter with this.props.history.push('/page') in React-Router v4?
.then(response => {
var r = this;
if (response.status >= 200 && response.status < 300) {
r.props.history.push('/template');
});
First of all, you need not do var r = this; as this in if statement refers to the context of the callback itself which since you are using arrow function refers to the React component context.
According to the docs:
history objects typically have the following properties and methods:
length - (number) The number of entries in the history stack
action - (string) The current action (PUSH, REPLACE, or POP)
location - (object) The current location. May have the following properties:
pathname - (string) The path of the URL
search - (string) The URL query string
hash - (string) The URL hash fragment
state - (string) location-specific state that was provided to e.g. push(path, state) when this location was pushed onto the
stack. Only available in browser and memory history.
push(path, [state]) - (function) Pushes a new entry onto the history stack
replace(path, [state]) - (function) Replaces the current entry on the history stack
go(n) - (function) Moves the pointer in the history stack by n entries
goBack() - (function) Equivalent to go(-1)
goForward() - (function) Equivalent to go(1)
block(prompt) - (function) Prevents navigation
So while navigating you can pass props to the history object like
this.props.history.push({
pathname: '/template',
search: '?query=abc',
state: { detail: response.data }
})
or similarly for the Link component or the Redirect component
<Link to={{
pathname: '/template',
search: '?query=abc',
state: { detail: response.data }
}}> My Link </Link>
and then in the component which is rendered with /template route, you can access the props passed like
this.props.location.state.detail
Also keep in mind that, when using history or location objects from props you need to connect the component with withRouter.
As per the Docs:
withRouter
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 }.
Extending the solution (suggested by Shubham Khatri) for use with React hooks (16.8 onwards):
package.json (always worth updating to latest packages)
{
...
"react": "^16.12.0",
"react-router-dom": "^5.1.2",
...
}
Passing parameters with history push:
import { useHistory } from "react-router-dom";
const FirstPage = props => {
let history = useHistory();
const someEventHandler = event => {
history.push({
pathname: '/secondpage',
search: '?query=abc',
state: { detail: 'some_value' }
});
};
};
export default FirstPage;
Accessing the passed parameter using useLocation from 'react-router-dom':
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
const SecondPage = props => {
const location = useLocation();
useEffect(() => {
console.log(location.pathname); // result: '/secondpage'
console.log(location.search); // result: '?query=abc'
console.log(location.state.detail); // result: 'some_value'
}, [location]);
};
For the earlier versions:
history.push('/[pathToSomeWhere]', yourData);
And get the data in the related component just like below:
this.props.location.state // it is equal to yourData
For the newer versions the above way works well but there is a new way:
history.push({
pathname: '/[pathToSomeWhere]',
state: yourData,
});
And get the data in the related component just like below:
Class Component
this.props.location.state; // it is equal to yourData
Function Component
const location = useLocation();
location.state; // it is equal to yourData
Sometime it will be needed to use Link or NavLink component instead of using history.push function. you can use like below:
<Link
to={{
pathname: '/[pathToSomeWhere]',
state: yourData
}}
>
...
</Link>
Hint: the state key name should be used in the latest version.
you can use,
this.props.history.push("/template", { ...response })
or
this.props.history.push("/template", { response: response })
then you can access the parsed data from /template component by following code,
const state = this.props.location.state
Read more about React Session History Management
If you need to pass URL params
theres a great post explanation by Tyler McGinnis on his site, Link to the post
here are code examples:
on the history.push component:
this.props.history.push(`/home:${this.state.userID}`)
on the router component you define the route:
<Route path='/home:myKey' component={Home} />
on the Home component:
componentDidMount(){
const { myKey } = this.props.match.params
console.log(myKey )
}
React TypeScript with Hooks
From a Class
this.history.push({
pathname: "/unauthorized",
state: { message: "Hello" },
});
UnAuthorized Functional Component
interface IState {
message?: string;
}
export default function UnAuthorized() {
const location = useLocation();
const message = (location.state as IState).message;
return (
<div className="jumbotron">
<h6>{message}</h6>
</div>
);
}
Pass
history.push({pathname:"/yourroute",state: {_id: "0001", name: "AZ"}})
Read
import React from 'react';
const YourRoute = props=> {
const { _id, name } = (props.location && props.location.state) || {};
//_id and name will contain the passed data
.
.
.
}
Here is a working example
I created a custom useQuery hook
import { useLocation } from "react-router-dom";
const useQuery = (): URLSearchParams => {
return new URLSearchParams(useLocation().search)
}
export default useQuery
Use it as
const query = useQuery();
const id = query.get("id") as string
Send it as so
history.push({
pathname: "/template",
search: `id=${values.id}`,
});
To use React 16.8 (withHooks) functional component you can use this way
We sending PhoneNumber to Next Page
Login.js
import { useHistory } from 'react-router-dom';
const history = useHistory();
const handleOtpVerify=(phoneNumber)=>
{
history.push("/OtpVerifiy",{mobNo:phoneNumber})
}
<button onClick={handleOtpVerify}> Submit </button>
OtpVerify.js
import useLocation from 'react-router-dom';
const [phoneNumber, setphoneNumber] = useState("")
useEffect(() => {
setphoneNumber(location.state.mobNo)
}, [location]);
return (
<p>We have sent Verification Code to your</p>
<h1>{phoneNumber}</h1>
)
react router dom version 6.2.1
useHistory() deprecated changed useNavigate()
import { useNavigate } from "react-router-dom";
const navigate = useNavigate()
onClick={() => { navigate('/OtpVerifiy',{mobNo:phoneNumber}) }}
You can use location to send state to other component, like this
In your Source Component
this.props.history.push(pathComponent, sendState);
pathComponent is target component that will receive the state
In your Target Component
you can receive the state like this if your use class component
Javascript version
constructor(props) {
this.state = this.props.location.state
}
Typescript version
constructor(props: {}) {
const receiveState = this.props.location.state as StateType // you must parse into your state interface or type
this.state = receiveState
}
Bonus
If you want to reset the received state. Use history to replace the location, like this
this.props.history({pathName: currentPath, state: resetState})
currentPath is the Target Component path
resetState is new value state whatever you want
It is not necessary to use withRouter. This works for me:
In your parent page,
<BrowserRouter>
<Switch>
<Route path="/routeA" render={(props)=> (
<ComponentA {...props} propDummy={50} />
)} />
<Route path="/routeB" render={(props)=> (
<ComponentB {...props} propWhatever={100} />
)} />
</Switch>
</BrowserRouter>
Then in ComponentA or ComponentB you can access
this.props.history
object, including the this.props.history.push method.
To use React 16.8+(withHooks) you can use this way
import React from 'react';
import { useHistory } from 'react-router-dom';
export default function SomeFunctionalComponent() {
let history = useHistory(); // should be called inside react component
const handleClickButton = () => {
"funcionAPICALL"
.then(response => {
if (response.status >= 200 && response.status < 300) {
history.push('/template');
});
}
return ( <div> Some component stuff
<p>To make API POST request and redirect to "/template" click a button API CALL</p>
<button onClick={handleClickButton}>API CALL<button>
</div>)
}
Source here to read more https://reacttraining.com/react-router/web/example/auth-workflow
Add on info to get query parameters.
const queryParams = new URLSearchParams(this.props.location.search);
console.log('assuming query param is id', queryParams.get('id');
For more info about URLSearchParams check this link
URLSearchParams

Resources