Memory Router doesn't use the browsers back button - reactjs

Using react-router 4.2.2
Following the documentation at https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/MemoryRouter.md (Which is lacking a good example)
I have the following
<MemoryRouter
initialEntries={[
'/one',
'/two'
]}>
<div>
<Route path="/one" component={FirstPage} />
<Route path="/two" component={SecondPage} />
</div>
</MemoryRouter>
I can then use the following to easily navigate pages:
this.props.history.push('/two');
This works as intended. However, when I hit my browsers back button it does not take me back to the previous route.
While I could easily have navigation buttons on screen, I would love for the browsers back button to work as well. Can memory router do this? If so, how?

My understanding is that Memory Router specifically doesn't write anything to the browser's history, and as such you would need to handle this manually. One thing you can do is add an event listener:
componentDidMount() {
window.onpopstate = this.handleNavigation.bind(this);
}
handleNavigation(event) {
if (//event is going back) {
this.props.history.go(-1); // Or .goBack() if available, or whatever the equivalent is. Possibly will need to keep track of currentLocation in relation to an array and navigate that way.
} else if (//If event is going forward) {
this.props.history.go(1); // Or .goForward() if available
}
}

Related

How to add title of a article into URL in Reactjs?

I am trying to know if it is possible or not to add the title of an article into the URL after redirecting to a specific article?
An example:
https://www.analyticsinsight.net/top-50-programming-languages-learn-coding/
so I am using reactjs with nodejs and MySQL.
I have a table with their columns, post_Text, post_Title, etc.
I only need to add the title of the article to the URL after User clicks on the title of a specific article.
i have a router as follows.
<Route path="/post/:id" exact>
<Post />
</Route>
and in the post.js file, I have a simple get method.
axios.get(`${server}/posts/byId/${id}`).then((res) => {
setPosts(res.data);
});
i would appreciate your help:_)
a well put question. I asked myself the same. Here is how I did it.
I set up the route, much like you have but with a second route that had another level of depth and another param on the end.
<Route path="/post/:id" exact><Post /></Route>
<Route path="/post/:id/:post_title" exact><Post /></Route>
On the click handler that navigates to the post, I did this. The values passed here. The regex simply identifies white spaces so that they can be replaced with a string.replace method.
const handleReadPost = (post_id, post_title) => {
const urlRegex = /\s/g;
const url_title = post_title.toLowerCase().replace(urlRegex, '-');
navigate(`/posts/${post_id}/${url_title}`);
};
In my case, it then just worked. I didn't need to make any adjustments to my Axios GET or logic in the Post component.

Generate Route dynamically - React

I am trying to do RBAC(Role-Based Access Control) using react js. I have seen some related question on Stackoverflow but they are not related whith my question. My question is:
There is a user and the user has a role and every role has its own list of menus assigned to it. Each menu has a path and component name.
What I need to do is that when a user sign in Route will be generated dynamically based on his role.
As we know the following is the normal way of defining routes in React. and it is static.
<Switch>
<Route path="/users" component={Users} exact />
<Route path="/items" component={Items} exact />
....
</Switch>
I need to generate every Route dynamically. This is what I have tried,
menus=[
{
name:"Users"
url:"/users"
component:"Users"
},
{
name:"Items"
url:"/items"
component:"Items"
}
]
<Switch>
{
menus.map(menu =>{
return(<Route path={menu.url} component={menu.component} exact/>)
})
}
</Switch>
But this is not working for me. When I try to navigate to /users it can't find the route. But when I use the first method it works correctly. Can you please tell me whats wrong with my code??
Here is a sample code that describes my problem
Here is a sandbox link
You are passing the component as a string to Route, you need to remove the quotes around them in your menu array.
menus=[
{
name:"Users"
url:"/users"
component:Users
},
{
name:"Items"
url:"/items"
component:Items
}
]
If your menu is dynamic from a DB and they are strings, then you will need to make sure the component is imported into the file, and then map it with an object like so:
import {Users} from './Users';
// assuming this is what menu will return as
const menus=[
{
name:"Users"
url:"/users"
component:"Users"
},
{
name:"Items"
url:"/items"
component:"Items"
}
]
const keyMap = {
Users: Users // mapping 'Users' string to <Users/> component
}
return (<Route path={menu.url} component={keyMap[menu.component]} exact/>)
Solution demonstration is available here

React (Native) Context API Causes Stack Navigator (React Navigation 5) to re-render after state update

Thanks in advance for helping with this question.
Some Context (No Pun Intended)
I'm working on a company's project that will restrict me from putting all the code on here, but I will try and put as much relevant code as I can without revealing state secrets.
Basically, the React Native app is using React Navigation v5 and I've been trying to figure this problem out for two days. I understand what is happening, but I don't understand why its happening or how to fix it.
Somewhere inside the app code, you have the below code.
export const Lounge = function Lounge(props: {navigation: any}) {
const {navigation: {setOptions}} = props
const bottomTabBarCtx = useContext(BottomTabBarContext)
// const [routeName, setRouteName] = useState("LoungeList")
useEffect(() => {
setOptions({tabBarVisible: bottomTabBarCtx.isVisible})
}, [bottomTabBarCtx.isVisible, setOptions])
return <Stack.Navigator initialRouteName={"LoungeList"}>
<Stack.Screen
name="LoungeList"
component={List}
options={{headerShown: false}}
/>
<Stack.Screen
name="LoungeDetails"
component={Details}
options={{
headerLeft : buttonProps => <BackButton {...buttonProps} />,
headerRight: buttonProps => <LoungeHeaderRightDetailsButton {...buttonProps} />,
headerTitle: 'Lounge Details',
}}
/>
<Stack.Screen
name="LoungeParticipants"
component={Participants}
options={{
headerLeft : buttonProps => <BackButton {...buttonProps} />,
headerRight: buttonProps => <LoungeHeaderRightDetailsButton {...buttonProps} />,
headerTitle: 'Lounge Participants',
}}
/>
<Stack.Screen
name="LoungeAdd"
component={LoungeEdit}
options={{
headerLeft : buttonProps => <BackButton {...buttonProps} />,
headerTitle: 'Create Lounge',
}}
/>
<Stack.Screen
name="LoungeEdit"
component={LoungeEdit}
options={{
headerLeft : buttonProps => <BackButton {...buttonProps} />,
headerTitle: 'Edit Lounge',
}}
/>
...
The above file simply defines the routes that can be navigated to in this section of the app.
The initial screen/route name = 'LoungeList'
That all works fine.
From the LoungeList screen, I tap on a lounge summary cell (use your imagination please), and that tap takes me to the screen underneath: LoungeDetails
Inside the LoungeDetails screen, I see the details of the lounge, and on that screen, there is also a Join Button. This button enables me to join the lounge.
The functionality is supposed to be that, when a user taps on the join button, the button goes into a loading state, and when it has finished loading, it should read Joined. This happens as it should BUT immediately after this happens I am navigated to the LoungeList screen
Below is the code I execute for joining a lounge:
const userDidJoinLounge = (joinedLounge: LoungeWithUsers) => {
if(joinedLounge){
console.log("joinedLounge before update == ", joinedLounge);
const allLoungesNew = LoungeController.replaceLoungeInLoungeArray(
userCtx.allLoungesList,
joinedLounge
);
const userLoungesNew = LoungeController.getUserJoinedLoungeArrayListOnJoin(
allLoungesNew,
userCtx.userLoungeList,
joinedLounge
);
console.log("allLounges after update == ", allLoungesNew);
console.log("joinedLounges after update == ", userLoungesNew);
userCtx.updateLoungeLists(allLoungesNew, userLoungesNew)
}
}
In the above function, the line
userCtx.updateLoungeLists(allLoungesNew, userLoungesNew) takes in two parameters, a list of all the lounges on the app, and a list of lounges the user has joined. This function updates the context that holds these two values.
THE BIG ISSUE
Issue: I keep getting redirected to the LoungeList screen, even though there isn't any code instructing the app to do that.
THINGS THAT MIGHT HELP
I've played around with many things and a few things that might help you in figuring out what is going on are:
It seems that the React Navigation route config is being re-rendered. I say this because when I changed the initialRouteName to be LoungeDetails, the lounge details screen (where the join button is) flickers, but it stays on the same screen. This also tells me, that at the end of that function, the stack navigator falls back to whatever initialRouteName is.
Wierdly, when I only set allLoungesNew then the app doesn't navigate to initialRouteName, it seems this issue only happens when I set userLoungesNew in the userContext's state.
How do I stop react-navigation from navigating away from the screen when the user has tapped the join button?
Thank you very much for your help. I hope I've given you enough information to solve the problem. Feel free to ask any more questions to help solve the problem. I will tell you what I am allowed to.
After about 3 days of searching the internet for an answer, the answer was pretty simple.
I had found numerous articles and suggestions telling me not to put any code for creating navigators inside the component itself.
I didnt write the original code for this app, I came on about 2 months ago, and while I had looked at the nested navigators, It didn’t immediately occur to me that the problem might be higher up the navigation chain/stack.
In the root index.tsx file, the former developers had put the root navigator (the navigator that held all the other sub/nested navigators) inside the entry point component.
This file was the only place that any navigator was being created within the component, but I didn’t think to look there because the behaviour seemed localised to the lounge section of the app
The Solution
Anyway, the fix was simple. In the root index.tsx file, I moved every navigator declaration out of the (root) functional component, and that fixed the issue.
It was clear that the navigator was being unmourned/remounted whenever I called a state change on a state held in my UserContext, it makes sense now that because the code to create the navigators was inside a component, whenever I would change the state, the navigators would be re-rendered as well, which took me back to the initialRouteName screen.
Main Lesson Learned
React Navigation v5 unlike its earlier versions now allows us to declare navigators with JSX tags, which gives us more flexibility by providing the opportunity for things like dynamic configurations of our navigators, however we have to be careful about where any/all of our navigators are declared
After 3 days on this problem, everything pointed to this solution, but I didn’t think that was the case in the code I was working on because with the exception of one file, all other navigator declarations were made outside the component.
It could be the same for you. If you’re getting that kind of behaviour when your state changes, then the problem navigator may be way up your chain of navigators.
I hope this helps someone! 😊

How to access different components by routes with slug only in react-router-dom?

Lets say we have two components for Post and Project. Each post and project has slug. I want to access post and projects by slug. So we have routes:
<Route path="/:post_slug" component={PostComponent} />
<Route path="/:project_slug" component={ProjectComponent} />
And for example when you visit some post pr project, an action gets called to fetch its data by params. Something like this:
this.props.fetchPost(this.props.match.params.post_slug)
this.props.fetchProject(this.props.match.params.project_slug)
If I do only one component accessible by slug only, then it works. But if I want to access two different components by slug, then it's messed up. And i.e. when I visit post, react thinks that I'm visiting project.
Maybe there is some way to handle such case in react-router-dom?
There is no way to do this, since the router can't differentiate between the two routes. For example, if I were to navigate to site.com/my_slug, the router wouldn't know whether my_slug is supposed to be a :post_slug or a :project_slug. It's sending you to a project when they're both present because it interprets them to be the same route and so it uses the last one you define.
An alternative you could try is being a bit more explicit with your routes, like so:
<Route path="/post/:post_slug" component={PostComponent} />
<Route path="/project/:project_slug" component={ProjectComponent} />
You could do this if you put the detection logic in another component, like this:
<Route path="/:any_slug" component={AnyComponent} />
Then let that component figure out what type of ID you have and render the desired sub-component accordingly:
const AnyComponent = props => {
if (slugIsProject(props.match.params.any_slug)) {
return <ProjectComponent id={props.match.params.any_slug} />;
} else {
return <PostComponent id={props.match.params.any_slug} />;
}
}
This way the router always renders the same component (AnyComponent) but that component appears differently depending on what the ID is determined to be.

How to prevent route change using react-router

There's a certain page in my React app that I would like to prevent the user from leaving if the form is dirty.
In my react-routes, I am using the onLeave prop like this:
<Route path="dependent" component={DependentDetails} onLeave={checkForm}/>
And my onLeave is:
const checkForm = (nextState, replace, cb) => {
if (form.IsDirty) {
console.log('Leaving so soon?');
// I would like to stay on the same page somehow...
}
};
Is there a way to prevent the new route from firing and keep the user on the same page?
It is too late but according to the React Router Documentation you can use preventing transition with helping of <prompt> component.
<Prompt
when={isBlocking}
message={location =>
`Are you sure you want to go to ${location.pathname}`
}
/>
if isBlocking equal to true it shows a message. for more information you can read the documentation.
I think the recommended approach has changed since Lazarev's answer, since his linked example is no longer currently in the examples folder. Instead, I think you should follow this example by defining:
componentWillMount() {
this.props.router.setRouteLeaveHook(
this.props.route,
this.routerWillLeave
)
},
And then define routerWillLeave to be a function that returns a string which will appear in a confirmation alert.
UPDATE
The previous link is now outdated and unavailable. In newer versions of React Router it appears there is a new component Prompt that can be used to cancel/control navigation. See this example
react-router v6 no longer supports the Prompt component (they say that they hope to add it back once they have an acceptable implementation). However, react-router makes use of the history package which offers the following example for how to block transitions.
Note that to actually make this work in react router you have to replace the createBrowserHistory call with some hackery to make sure you are using the same history object as react router (see bottom of answer).
const history = createBrowserHistory();
let unblock = history.block((tx) => {
// Navigation was blocked! Let's show a confirmation dialog
// so the user can decide if they actually want to navigate
// away and discard changes they've made in the current page.
let url = tx.location.pathname;
if (window.confirm(`Are you sure you want to go to ${url}?`)) {
// Unblock the navigation.
unblock();
// Retry the transition.
tx.retry();
}
You'll need to put this inside the appropriate useEffect hook and build the rest of the functionality that would have otherwise been provided by prompt. Note that this will also produce an (uncustomizable) warning if the user tries to navigate away but closing the tab or refreshing the page indicating that unsaved work may not be saved.
Please read the linked page as there are some drawbacks to using this functionality. Specifically, it adds an event listener to the beforeunload event which makes the page ineligable for the bfcache in firefox (though the code attempts to deregister the handler if the navigation is cancelled I'm not sure this restores salvageable status) I presume it's these issues which caused react-router to disable the Prompt component.
WARING to access history in reactrouter 6 you need to follow something like the instructions here which is a bit of a hack. Initially, I assumed that you could just use createBrowserHistory to access the history object as that code is illustrated in the react router documentation but (a bit confusingly imo) it was intended only to illustrate the idea of what the history does.
We're using React Router V5, and our site needed a custom prompt message to show up, and this medium article helped me understand how that was possible
TLDR: the <Prompt/> component from react-router-dom can accept a function as the message prop, and if that function returns true you'll continue in the navigation, and if false the navigation will be blocked
React-router api provides a Transition object for such cases, you can create a hook in a willTransitionTo lifecycle method of the component, you are using. Something like (code taken from react-router examples on the github):
var Form = React.createClass({
mixins: [ Router.Navigation ],
statics: {
willTransitionFrom: function (transition, element) {
if (element.refs.userInput.getDOMNode().value !== '') {
if (!confirm('You have unsaved information, are you sure you want to leave this page?')) {
transition.abort();
}
}
}
},
handleSubmit: function (event) {
event.preventDefault();
this.refs.userInput.getDOMNode().value = '';
this.transitionTo('/');
},
render: function () {
return (
<div>
<form onSubmit={this.handleSubmit}>
<p>Click the dashboard link with text in the input.</p>
<input type="text" ref="userInput" defaultValue="ohai" />
<button type="submit">Go</button>
</form>
</div>
);
}
});

Resources