Intercepting and manipulating browser history with react router history - reactjs

I'm trying to intercept history call in react app and manipulate it to change search parameter of url when it tries to route to certain path. I'm successfully on listening but changing location parameters or replacing history (I want to add ?default_param=value) is not working. It still change location using old parameters. My example code:
import createHistory from 'history/createBrowserHistory';
const history = createHistory();
history.listen(() => {
if (
history.location.pathname === '/path-to-manipulate' &&
!history.location.search
) {
const unblock = history.block();
// this one doesn't change current redirection
history.push({
pathname: '/path-to-manipulate',
search: '?default_param=value',
});
/* this one is also not working
history.replace({
pathname: '/path-to-manipulate',
search: '?default_param=value',
});
*/
unblock();
}
});
Long time ago there was history.listenBefore() https://github.com/ReactTraining/history/issues/379, but it was removed from api and now we have history.block(). I tried with and without block, with different combination of replace/push/forward and event changing history.location explicite. Nothing of that would allow me to properly intercept location before change, alter it and execute with new parameter ?default_param=value.
How to properly intercept location change and change it (location params) before it executes and redirect?
Two more things:
I don't want to mess with <Link to=...>
components and implement logic there. I wanted something global that could manage routing on higher level in react app.
listen() won't work when entering url in browser, it only works when using routing in app. Is there another way to catch first reference on certain location in react app?

What I usually do is try to read the default_param value, and if there is nothing I return with <Redirect to... />
"I wanted something global that could manage routing on higher level in react app."
Why is it? You want a global solution for a problem which is very specific?
If this problem happens a lot (you need a default param), you can write a HOC which takes a parameter and a default value and either return with a <Redirect /> or the wrapped component depends on you have the parameter in the url

Related

Pass state via Link while redirected to a new tab

case 1:
Redirected to a new route in the same tab.
<Link to={{ pathname: "/modify_items", state: { foo: "bar" } }} >
When I type console.log(this.props.location.state); the state { foo: "bar" } get printed.
case 2:
Redirected to a new route in a new tab.
<Link to={{ pathname: "/modify_items", state: { foo: "bar" } }} target="_blank" >
When I type console.log(this.props.location.state); the state becomes undefined.
My goal is to pass data to a new tab. Please guide me if there is any other way to achieve this in React.js. I just want to pass data to a new tab.
I think this is not possible with Link state. Cause if you leave the page your memory will be clean. You might want to use query or path params if you want to share state over different tabs.
For path params:
Get path params in react-router v4
For query variables:
Set the Link as followed:
<Link to={{ pathname: '/foo', query: { foo: "bar" } }}/>
and use maybe a library to get the query param. Such as https://www.npmjs.com/package/query-string.
Since the _blank opens a new tab so it is another instance of your app even the app is the same app the internals are different. But you can use something like localStorage.
Since they will be persisted and will be shared for same origin you will have access to it in the new or any tab or window of your application. You need to query it in the component mount or after first render with and useEffect hook with a second parameter being probably empty array, []. For hooks checkout React docs if you use them.
Another way is to use the querystring. You can receive the query string from your opened comopenent instance.
I am sure you can use any other persistense mechanism, like Indexed DB, Cookies, etc.
There are a few other ways coming into my mind under the umbrella web messaging; Broadcast Channel API, Window.postMessage, and Channel Messaging API that you may make use of.
If you state is relatively small it mifght be good to use querystring, or localStorage is the easiest way to go. If your data way more bigger you can use Indexed DB. It is async. Only be careful when it completes write, then you get it in your new window, tab.
In persistence usage cases, write to persistence before you navigate, just wait the write process if it is async before you navigate (by opening new tab/window in your specific use case).
One might navigate to a new tab maybe by right clicking and selecting from the context menu. ın that case Page Visibility API can be used on the right clicked page in a React effect (attaching its effects in an React effect with an empty second argument to React effect) and the data can be written again to a persistence mechanism, one of above mentioned ones, and the rest I mean the operations in the opening tab will be the same.
Since the writing mechanism can be late and even it is a sync one opening of a new tab if it is not programmatical might happen before the writing is done. In that case the opening tab's React effect to read the data written might be early to read. To circumvent these workarounds like waiting via a setTimeout to read can be used simply, however, a programmatically correct and a guaranteedly timed way would be using one of the appropriate web messaging apis I have mentioned above like Broadcast Channel, postMessage, and Channel Messaging API.
It is not possible to make it via React Router.
But you may use localStorage to save something there before you redirect to new tab and then check if something exists in localStorage on new tab.
It's working for me:
<Link to="/onboarding/profile" state={{ from: "occupation" }}>
Next Step
</Link>
next step:
import { useLocation } from 'react-router-dom'
function Profile () {
const location = useLocation()
const { from } = location.state
return (
...
)
}

How do you pass data from one view to the other via react-router-dom without using url parameters?

I use react-router-dom v 4.3.1 for client-side routing. I'm pretty new to React and can't figure out how to pass data from one view to the other without using url parameters. In Angular, the Angular router has a data property where you can pass data associated with a route. An example would be:
const appRoutes: Routes = [
{
path: 'hero/:id',
component: HeroDetailComponent,
data: { title: 'Hero Detail' }
},
];
Can you do the same in react-router-dom? If not, how would you recommend I pass data in React?
Thanks in advance for the help!
<Route path="hero/:id" render={() => <HeroDetailComponent title= "Hero Detail" />} />
Read this: Pass props to a component rendered by React Router
Or if you are using <Link> you can use pass through location object
<Link to={{ pathname: 'hero/:id', state: { title: 'Hero Detail'} }}>My route</Link>
Well you Could use the context API to create a sort of global AppState that you could update in your first component and use in your second component.
You could also abuse the localStorage API by setting a key with the data in the first component and getting it in the other.
However both of these are workarounds that Shouldn't have to be used. Why do you want to redirect to a page but not pass data to it using URL parameters.
There'a several solutions. React being a library, not a framework, doesn’t force you into a single one.
One way is to use the context api. It’s like a link to an object shared between different components.
Another one is redux, which uses context underneath, and gives you a single store for the whole app. You changes values dispatching actions to the store, so it’s a bit tricky to learn the first time.
Using a stream library would open up a lot of different options, but it’s harder to get into. Check refract if you want to go this way.
A poor man’s stream approach that may serve you is using document as a bus to pass data arround, using addEventListeners to receive data and dispatch new customEvent to send it.
Next is the simplest one of all, share a simple object. Using imports form your components, you can import the same object on both and that will be a single instance where data can be shared. Simple JavaScript. It’s not the react way though, because changes won’t trigger a repaint on the component.

Pass data to react routes

When transitioning from one react route to another, I can pass strings via URL parameters
this.props.history.replace({
pathname: `${this.props.match.url}/newRoute`,
search: '?someData=someValue'
})
But what if I want to pass something more complex, e.g. a deeply nested JSON object? Is there a way such data can be passed from one route to another without using Redux?
The project in question is using react 14.4.2 and react-router 4.3.1.
Passing complex data between routes is usually a bad idea. For example, what happens when the user refreshes the page on the route? When the page reloads, all the app state and whatever you passed through to the route will have been lost.
As a rule of thumb, each route should be capable of gathering or refreshing all of the data it needs from scratch using only the information in the url (paths, path params, or query strings).
If that doesn't quite fit with your use case, consider the possibility that this part of your app doesn't need its own bookmarkable route/url, and you're just implementing a transitory UI change which is ok if it gets lost upon page refresh.
Example for passing parameter in Route:
<Route path="/:id" component={Child} />
const Child = ({ match }) => (
<div>
<h3>ID: {match.params.id}</h3>
</div>
);
please refer : https://reacttraining.com/react-router/web/example/url-params
I think possible ways can be:
1- By using location state object, we can pass the data into that and it will be available in new page. Issue with this approach will be, after refreshing the new page, that data will not be available.
Like this:
this.props.history.replace({
pathname: `${this.props.match.url}/newRoute`,
search: '?someData=someValue',
state: {
....
}
})
And access it in new page by: this.props.location.state
2- Using localStorage, set the data into localStorage before navigating to new page and then read the same data in new page.
Like this:
navigate() {
localStorage.setItem('data', obj);
this.props.history.replace({
pathname: `${this.props.match.url}/newRoute`,
search: '?someData=someValue',
})
}
I will suggest to use the 2nd approach, its not a good idea of passing big object in route.

React router -- How to send props without query params or Redux?

I want to send data to another route, but don't want to send it in query params.
I don't want a new store for every route, nor do I want a store that simply holds all routes / params separately from where they are sent / consumed.
Is there a standard way to specify props for an upcoming route?
I found the solution on the react-router location api docs.
this.props.router.push({
pathname: '/view-user',
state: { userId }
});
This seems great for interstitial, standalone modal pages.
May need to specify a fallback if the state is missing, but haven't quite gotten that far.
if (!this.props.location.state) this.props.router.goBack();
or
const locations = this.props.location.pathname.split('/');
// do something
this.props.route.push(locations.join('/'));
If you are not sending the information in the query param, then you can put it in some other kind of store that can also be associated with the route.
You can wrap the router.push() call with your own function that takes an extra object you want to pass along. Something like...
function navigateTo(url, extraData) {
if (extraData !== undefined) {
saveExtraDataForRoute(url, extraData);
}
router.push(url);
}
In react-router, there is an onEnter prop associated with the route that specifies a function to call. The code in this function can retrieve the extra data and do whatever you want to do with it.
function onMyScreenEnter() {
const extraData = getExtraDataForRoute(url);
goCrazyNutsWithYourExtraData(extraData);
}
You'd supply the two functions saveExtraDataForRoute() and getExtraDataForRoute(). They could use your store (e.g. Redux), set values of a singleton object, or use LocalStorage. But essentially, to save the data so it's retrievable by URL later, you'd be saying something like:
extraDataStore[url] = extraData;
The other thing you may wish to look into is using a POST method with react-router. I haven't done this, and am not sure how well it works. Here is a link: How to Handle Post Request in Isomorphic React + React Router Application

React-redux get site base URL / window.location

Hopefully this is a very simple question:
I want to create a string containing the full URL to a page on my site, like:
https://example.com/documents/1
Ideally I'd like to generate this in a react-redux connect()-ed container, in mapStateToProps(). Where the page is a grandchild of a react-router Route using browserHistory (so maybe I can get it from react-router or browserHistory somehow?) If necessary I'd do it in a full-on React class.
But I can't for the life of me find out how to do this. window (as in window.location) is always undefined in mapStateToProps() (no big surprise there) and in my view components' render() function.
So far I have only found ways to access the current route, e.g. "/documents/1", which is only a relative path and does not contain the site base url (https://example.com/)
So firstly, remember your code runs on client and server so any access to window needs to be wrapped in a check.
if (typeof window !== 'undefined') {
var path = location.protocol + '//' + location.host + '/someting'; // (or whatever)
} else {
// work out what you want to do server-side...
}
If the URL you generate shows in the DOM, and on the client you populate the store with this URL before the first render, you're going to get a checksum error. If you really want to avoid the error you can wait until componentDidMount() in your root component to get/set the url.
After componentDidMount you can have direct access to the origin location so that you can get the root url.
I use the below code in my JSX all the time so that i dont have to worry about the root path ex: dev env this would be localhost:3000/whatever, staging this will be AppStaging.heroku/whatever/..... and in production it would evaluate to www.myapp.com/whatever
`${window.location.origin.toString()}/whateverRoute/`
You Can try!!
window.location.origin
you can try the URL API. Take a look at this link for more details https://developer.mozilla.org/en-US/docs/Web/API/URL
const url = new URL("blob:https://mozilla.org:443/")
console.log(url.origin);
// Logs 'https://mozilla.org'

Resources