React Router Push Silently - javascript

In my case, I have several tabs. The tab container can be swiped. If user switching tab, the route will also be updated.
For example:
If user is in Tab 1, the route will be /home. When he swipes to Tab 2, the route will be /home/news. When he swipes to Tab 3, the route becomes /home/about And so on.
I'm aware that it can be achieved with this.props.push(NEXTROUTE). But by doing that way, the component is re-rendered and makes me lose the last scrolled position of the previous tab. My case needs to keep that last scrolled position of the tab instead of scrolled over to the top.
Is it possible to push state silently in react router? I don't want the component to be re-rendered when pushing state so I can maintain the last scrolled position of the screen.
Any thoughts? Thanks.

One way is to set the routes to point to the same Component and use the route params to set the current chosen tab. This way you achieve the "silent" behaviour you are aiming for:
<Route path="/dashboard" component={App}/>
<Route path="/news/:itemId" component={App}/>
and:
componentWillReceiveProps() {
const { route, params } = props;
const { path } = route;
const selectedTab = path !== '/dashboard' ? 1 : 2;
this.setState({selectedTab})
console.log('news item -> ', params.itemId)
}
A second way is to save the scrolling position and set it back.
A working JSBin: https://jsbin.com/qiraqa/edit?js,output
Relevant code:
scroll(e) {
const { tab, scrollPositions } = this.state;
const target = e.target,
scrollTop = target.scrollTop;
scrollPositions[tab] = scrollTop;
this.setState({
scrollPositions
});
console.log(scrollTop);
}
navigateToTab(tab) {
const { scrollPositions = [] } = this.state,
scrollPosition = scrollPositions[tab] || 0;
this._container.scrollTop = scrollPosition;
this.setState({tab});
}
and:
<div style={MainStyles.overflow} onScroll={this.scroll.bind(this)} ref={(c) => this._container = c}>...
This way, you can use the localStorage or a redux store to persist the scrollPositions array and load it up again after route change.
Note that this example has nothing to do with React Tabs as IMHO, the issue is about saving scrolling position between two navigational states.
More information
A tab component, whether it's a react-tabs or material-ui's tabs, keeps the scrolling position by rendering all tabs and switching the visibility on and off when you move between tabs. It sets the Tab's style to {height: 0, overflow: hidden} and this way keeps the container alive with it's scroll position.
As you mentioned, this doesn't work when you change the root component, the scrolling position will reset when the component reloads. This is probably the proper behaviour, as the route cannot pre-know the side effects of a route change.
To quote Dan Abramov from this relevant Github thread:
...it's up to you to render your components the same way. For example if you don't cache the data locally, there's nothing router can do to restore your position. But that's how browser's default behavior works, too, we're just trying to emulate it
In other words, according to this paradigm, it's not the router's responsibility and should be done manually.

Related

react-navigation navigate.goBack() goes all the way to Home screen

so I am coding a simple app with react-navigation and I want to code this:
- Home (Tab)
- Profile (Tab)
-- User info (Stack screen)
--- Edit user info (Stack screen) - (screen with input to edit name, email etc...)
When I click save on the EditInfo screen I have a button on the right side of the header Done, this button should navigate back to the UserInfo screen where you can see the updated details.
BUT!
Everything works but when I click Done on the EditInfo screen, it navigates back to Home! Why is that?
Thanks for help
Could you please put the code of the service screen where you call the goBack function, it could be helpful. Generally you just call
You are either using the wrong Navigator comp or your requirements are not clear. Basically, You would like to use the StackNavigator for your desired behavior.
The catch is, DrawerNavigator is used to build up a drawer menu. If you swipe from the left you'll see your navigator drawer containing all of your screens as you can see in the image below
If you add a button on your screen like below, you'll see your menu open.
<Button title="MENU" onPress={() => this.props.navigation.navigate('DrawerOpen')} />
The conclusion is, whenever we use DrawerNavigator we always go back to initial route, which is whatever we defined as the first item or using the initialRouteName key of the second param of the DrawerNavigator.
It's only the StackNavigator that supports this stacking order you would like to achieve, as the name suggests itself.
What you can do is to wrap a new StackNavigator inside one of the screens of the DrawerNavigator. For example:
const AppNavigator = DrawerNavigator({
drawer1: {
screen: drawer1,
}
});
const drawer1 = StackNavigator({
one: { screen: one },
two: { screen: two },
three: { screen: three },
});
Maybe this answer would help you.
In a nutshell: maybe you need to specify backBehaviour param in your Tabs navigator.

Why does my React app menu open when I am only changing the state and am not using any CSS?

I have a button in my Header component. When it is clicked, it calls a toggleNav function stored in my context.js. This function changes the state of isNavOpen from false to true. The navgiation then opens. There is no CSS in my project that should allow this behavior. I also don't see any JS code that should allow this behavior either. Could someone tell me what code allows my navigation to open and close?
My codesandbox
This is down to the basic way that React works, when you change the state of a component, it re-renders itself with the new values you've set into state.
Specifically it's this bit of Header.js:
{context.state.isNavOpen && (
<div className="js-nav nav">
...
When the component renders the first time, context.state.isNavOpen is false, and false && anything is still false, so javascript ignores the code after the &&. That means it skips over the menu code.
The second time it renders, after you update the state which is pushed to context and then passed to <Header> as a prop (!), the component re-renders with your menu code.
If you use your browser's dev tools to inspect the DOM before and after you click the button, you'll find that the menu isn't hidden and shown, but rather when you don't see it, it's gone from the DOM altogether.
It's react feature whenever state changes component re-renders
same thing happening here
{context.state.isNavOpen && (
isNavOpen is toggling (true to false), (false to true)
for example - you can check it simply
class Toggle extends React.Component {
state = {
visibility: false
}
toggleVisibility=()=>{
this.setState(prev=>({visibility:!prev.visibility}))
}
render() {
const {visibility} = this.state;
return (
<div>
<button onClick={this.toggleVisibility}>Click Me</button>
<h3>{visibility?'Welcome':''}</h3>
</div>
);
}
};

How to unmount inactive screens in bottom tab navigator react-navigation?

Im using react-navigation for react-native. Is there an option to make that inactive tab screens get unmounted like unmountInactiveRoutes: true in DrawerNavigator?? I cant find something like unmountInactiveRoutes for BottomTabNavigator.
I have two stacknavigators inside a BottomTabNavigator and I want to unmount them automatically.
BottomTabNavigator
Stack1
Screen
Screen
Stack2
Screen
Screen
You can unmount screens in bottom tab by adding option in navigation screenOptions (or in Tab.Navigator screenOptions):
unmountOnBlur: true
You can do it in Tab & Drawer Navigations but not in Stack Navigation.
And you can also add unmount individual screen by adding same option in Tab or Drawer Screen option.
So I don't know if you can unmount components that are inactive personally I did not find it however this is my workaround withNavigationFocus(FocusStateLabel)
and if isFocused is false. returning null. So this will give you more or less what you are looking for. If isFocused is true, you'll render what you usually render. If false you'll return null. resulting in the unmounting of your components
Some reference https://reactnavigation.org/docs/en/with-navigation-focus.html
I tried Ubaid’s answer it works. But you can try this one too:
Use
import {useIsFocused} from '#react-navigation/native';
const isFocused = useIsFocused();
useEffect(() => {
// Do whatever you want to do when screen gets in focus
}, [props, isFocused]);
It works perfectly fine.
I found two way unmount.
First method is just trigger the unmount using useFocusEffect. With my experience this is not completely unmount component. It just trigger only unmount function to unsubscribe events.
https://reactnavigation.org/docs/function-after-focusing-screen/#triggering-an-action-with-a-focus-event-listener
Second method is completely unmount component when the navigating. This one is working as react unmount.
https://reactnavigation.org/docs/bottom-tab-navigator/#unmountonblur
<Tab.Navigator screenOptions={{unmountOnBlur: true}}>
</Tab.Navigator>
In your tab screens
const unMount = ()=>{
//unmount what you want
}
useEffect(()=>{
return unMount;
},[])
Modify your code with this

Make a component stick to screen with a message. React

What I want to do is to make a nice message box that appears when I'm saving something, I have access to a state that tells me if it's loading or not, but I can't seem to make a component that always appears, no matter how much you've scrolled.
The page is pretty long so wherever they click on a button that triggers this save function, I want this to appear. Where they are.
I've used Semantic UI in this project, and I've tried their sticky component but it only makes the component follow the screen like 50px, I want that number to be infinite.
Thankful for every answer
You could also achieve this with a fixed modal I guess.
Inside your main component:
state = {
// (...)
isLoading: false
}
// (...)
render() {
return (
// Rest of your code
{this.state.isLoading && <MessageComponent/>} // or however you're rendering the message/modal
// Rest of your code
)
}
Then in your respective css file styling your MessageComponent:
.messageComponent {
position: fixed;
}

How to handle scroll position on hashchange in Backbone.js application?

I've read through many related threads but none of them seem to provide a solution.
What I'm trying to do is handle the scrollbar intelligently in my Backbone.js app. Like many others, I have multiple #mypage hash routes. Some of these routes are hierarchical. e.g. I have a #list page that lists some items, I click on an item in the list. Then it opens up a #view/ITEMID page.
My pages all share the same Content div in the HTML layout. On a navigation change, I inject a new div representing the view for that route into the Content div, replacing whatever was there before.
So now my problem:
If the item is far down in the list I might have to scroll to get there. When I click on it, the "default" Backbone behavior is that the #view/ITEMID page is displayed at the same scroll position that the #list view was. Fixing that is easy enough; just add a $(document).scrollTop(0) whenever a new view is injected.
The problem is if I hit the back button I would like to go back to the #list view at the scroll position it was previously.
I tried to take the obvious solution to this. Storing a map of routes to scroll positions in memory. I write to this map at the beginning of the handler for the hashchange event, but before the new view is actually put into the DOM. I read from the map at the end of the hashchange handler, after the new view is in the DOM.
What I'm noticing is that something, somewhere, in Firefox at least, is scrolling the page as part of a hashchange event, so that by the time my write-to-map code gets called, the document has a wonky scroll position that was definitely not explicitly made by the user.
Anyone know how to fix this, or a best practice that I should be using instead?
I double checked and there are no anchor tags in my DOM that match the hashes I'm using.
My solution to this ended up being something less automatic than I wanted, but at least it's consistent.
This was my code for saving and restoring. This code was pretty much carried from my attempt over to my actual solution, just called it on different events. "soft" is a flag that this came from a browser action (back, forward, or hash click) as opposed to a "hard" call to Router.navigate(). During a navigate() call I wanted to just scroll to the top.
restoreScrollPosition: function(route, soft) {
var pos = 0;
if (soft) {
if (this.routesToScrollPositions[route]) {
pos = this.routesToScrollPositions[route];
}
}
else {
delete this.routesToScrollPositions[route];
}
$(window).scrollTop(pos);
},
saveScrollPosition: function(route) {
var pos = $(window).scrollTop();
this.routesToScrollPositions[route] = pos;
}
I also modified Backbone.History so that we can tell the difference between reacting to a "soft" history change (which calls checkUrl) versus programmatically triggering a "hard" history change. It passes this flag to the Router callback.
_.extend(Backbone.History.prototype, {
// react to a back/forward button, or an href click. a "soft" route
checkUrl: function(e) {
var current = this.getFragment();
if (current == this.fragment && this.iframe)
current = this.getFragment(this.getHash(this.iframe));
if (current == this.fragment) return false;
if (this.iframe) this.navigate(current);
// CHANGE: tell loadUrl this is a soft route
this.loadUrl(undefined, true) || this.loadUrl(this.getHash(), true);
},
// this is called in the whether a soft route or a hard Router.navigate call
loadUrl: function(fragmentOverride, soft) {
var fragment = this.fragment = this.getFragment(fragmentOverride);
var matched = _.any(this.handlers, function(handler) {
if (handler.route.test(fragment)) {
// CHANGE: tell Router if this was a soft route
handler.callback(fragment, soft);
return true;
}
});
return matched;
},
});
Originally I was trying to do the scroll saving and restoring entirely during the hashchange handler. More specifically, within Router's callback wrapper, the anonymous function that invokes your actual route handler.
route: function(route, name, callback) {
Backbone.history || (Backbone.history = new Backbone.History);
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
if (!callback) callback = this[name];
Backbone.history.route(route, _.bind(function(fragment, soft) {
// CHANGE: save scroll position of old route prior to invoking callback
// & changing DOM
displayManager.saveScrollPosition(foo.lastRoute);
var args = this._extractParameters(route, fragment);
callback && callback.apply(this, args);
this.trigger.apply(this, ['route:' + name].concat(args));
// CHANGE: restore scroll position of current route after DOM was changed
// in callback
displayManager.restoreScrollPosition(fragment, soft);
foo.lastRoute = fragment;
Backbone.history.trigger('route', this, name, args);
}, this));
return this;
},
I wanted to handle things this way because it allows saving in all cases, whether an href click, back button, forward button, or navigate() call.
The browser has a "feature" that tries to remember your scroll on a hashchange, and move to it when going back to a hash. Normally this would have been great, and would save me all the trouble of implementing it myself. The problem is my app, like many, changes the height of the DOM from page to page.
For example, I'm on a tall #list view and have scrolled to the bottom, then click an item and go to a short #detail view that has no scrollbar at all. When I press the Back button, the browser will try to scroll me to the last position I was for the #list view. But the document isn't that tall yet, so it is unable to do so. By the time my route for #list gets called and I re-show the list, the scroll position is lost.
So, couldn't use the browser's built-in scroll memory. Unless I made the document a fixed height or did some DOM trickery, which I didn't want to do.
Moreover that built-in scroll behavior messes up the above attempt, because the call to saveScrollPosition is made too late--the browser has already changed the scroll position by then.
The solution to this, which should have been obvious, was calling saveScrollPosition from Router.navigate() instead of the route callback wrapper. This guarantees that I'm saving the scroll position before the browser does anything on hashchange.
route: function(route, name, callback) {
Backbone.history || (Backbone.history = new Backbone.History);
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
if (!callback) callback = this[name];
Backbone.history.route(route, _.bind(function(fragment, soft) {
// CHANGE: don't saveScrollPosition at this point, it's too late.
var args = this._extractParameters(route, fragment);
callback && callback.apply(this, args);
this.trigger.apply(this, ['route:' + name].concat(args));
// CHANGE: restore scroll position of current route after DOM was changed
// in callback
displayManager.restoreScrollPosition(fragment, soft);
foo.lastRoute = fragment;
Backbone.history.trigger('route', this, name, args);
}, this));
return this;
},
navigate: function(route, options) {
// CHANGE: save scroll position prior to triggering hash change
nationalcity.displayManager.saveScrollPosition(foo.lastRoute);
Backbone.Router.prototype.navigate.call(this, route, options);
},
Unfortunately it also means I always have to explicitly call navigate() if I'm interested in saving scroll position, as opposed to just using href="#myhash" in my templates.
Oh well. It works. :-)
A simple solution:
Store the position of the list view on every scroll event in a variable:
var pos;
$(window).scroll(function() {
pos = window.pageYOffset;
});
When returning from the item view, scroll the list view to the stored position:
window.scrollTo(0, pos);
I have a slightly poor-man's fix for this. In my app, I had a similar problem. I solved it by putting the list view and the item view into a container with:
height: 100%
Then I set both the list view and the item view to have:
overflow-y: auto
height: 100%
Then when I click on an item, I hide the list and show the item view. This way when I close the item and go back to the list, I keep my place in the list. It works with the back button once, although obviously it doesn't keep your history, so multiple back button clicks won't get you where you need to be. Still, a solution with no JS, so if it's good enough...
#Mirage114 Thanks for posting your solution. It works like a charm. Just a minor thing, it assumes the route functions are synchronous. If there is an async operation, for example fetching remote data before rendering a view, then window is scrolled before the view content is added to the DOM. In my case, I cache data when a route is visited for the first time. This is so that when a user hits browser back/forward button, the async operation of fetching data is avoided. However it might not always be possible to cache every data you need for a route.

Categories