When I navigate to a new page, Reach Router scrolls down to the page content past the header (if the content is long enough). I'm assuming this is for accessibility but it's not necessary for my app and it's actually quite jarring. Can this behaviour be disabled?
Note that I'm talking about Reach Router not React Router.
Reach Router
Try using <Router primary={false}> which will not focus on the route component.
https://reach.tech/router/api/Router
primary: bool
Defaults to true. Primary Routers will manage focus on location transitions. If false, focus will not be managed. This is useful for Routers rendered as asides, headers, breadcrumbs etc. but not the main content.
WARNING: If you are concerned about breaking accessibility please see this answer: https://stackoverflow.com/a/56996986/428780
The top answer here, while solving the OP's problem, is probably not the solution most people want, since it turns off the most important accessibility feature of Reach router.
The fact Reach router focuses the content of the matched <Route> on a route change is for accessibility reasons - so screen readers etc can be directed to the newly updated, relevant content, when you navigate to a new page.
It uses HTMLElement.focus() to do this - see the MDN docs here.
The problem is that by default, this function scrolls to the element being focused. There is a preventScroll argument which can be used to turn this behaviour off, but the browser support for it is not good, and regardless, Reach Router does not use it.
Setting primary={false} turns this behaviour off for any nested <Router> you may have - it is not intended to set false on your main (primary) <Router> -- hence the name.
So, setting primary={false} on your primary <Router>, as the top answer suggests, 'works' in the sense that it stops the scrolling behaviour, but it achieves this by simply turning off the focusing behaviour completely, which breaks the accessibility feature. As I said, if you do this, you're breaking one of the main reasons to use Reach Router in the first place.
So, what's the solution?
Basically, it seems that this side effect of HTMLElement.focus() - scrolling to the focused element - is unavoidable. So if you want the accessibility feature, you have to take the scrolling behaviour with it.
But with that said, there might be a workaround. If you manually scroll to the top of the page using window.scrollTo(0, 0) on every route change, I believe that will not 'break' the focusing feature from an accessibility perspective, but will 'fix' the scrolling behaviour from a UX perspective.
Of course, it's a bit of a hacky and imperative workaround, but I think it's the best (maybe only) solution to this issue without breaking accessibility.
Here's how I implemented it
class OnRouteChangeWorker extends React.Component {
componentDidUpdate(prevProps) {
if (this.props.location.pathname !== prevProps.location.pathname) {
this.props.action()
}
}
render() {
return null
}
}
const OnRouteChange = ({ action }) => (
{/*
Location is an import from #reach/router,
provides current location from context
*/}
<Location>
{({ location }) => <OnRouteChangeWorker location={location} action={action} />}
</Location>
)
const Routes = () => (
<>
<Router>
<LayoutWithHeaderBar path="/">
<Home path="/" />
<Foo path="/foo" />
<Bar path="/bar" />
</LayoutWithHeaderBar>
</Router>
{/*
must come *after* <Router> else Reach router will call focus()
on the matched route after action is called, undoing the behaviour!
*/}
<OnRouteChange action={() => { window.scrollTo(0, 0) } />
</>
)
Building off of #Marcus answer, you can get rid of the jank with useLayoutEffect() instead of useEffect() - this way the scroll action happens after the DOM has been fully rendered, so you don't get the weird "bounce."
// ScrollToTop.js
import React from 'react'
export const ScrollToTop = ({ children, location }) => {
React.useLayoutEffect(() => window.scrollTo(0, 0), [location.pathname])
return children
}
I had to use a combination of things to make it work. Even with setting primary={false} there were still cases where the pages would not scroll to the top.
<Router primary={false}>
<ScrollToTop path="/">
<Home path="/" />
<Contact path="contact-us" />
<ThankYou path="thank-you" />
<WhoWeAre path="who-we-are" />
</ScrollToTop>
</Router>
This is based off of React Router's scroll restoration guide.
The scroll to top component will still work with you don't have primary={false}, but it causes jank from where it focuses the route and then calls window.scrollTo.
// ScrollToTop.js
import React from 'react'
export const ScrollToTop = ({ children, location }) => {
React.useEffect(() => window.scrollTo(0, 0), [location.pathname])
return children
}
There must be another thing to cause this. Like #Vinicius said. Because for my application primary={false} really works. I have a small application and my routes below.
<Router primary={false}>
<Home path="/" />
<Dashboard path="dashboard" />
<Users path="users" />
<Sales path="sales" />
<Settings path="settings" />
</Router>
Using primary={false} does not solve all the cases. A common solution is to wrap the page and use useEffect to achieve the result. There might be a flash issue as someone pointed out, although it never happened to me, so try your luck.
<PageWrapper Component={About} path="/about" />
const PageWrapper = ({ path, Component }) => {
useEffect(() => {
window.scrollTo(0, 0)
}, [path])
return <Component path={path} />
}
As a notice, in React Router, you could use history.listen as explained here. I did not check if there is a similar solution in Reach Router, but that solution would be more optimal.
I made an npm package out of it to make integration simple: https://www.npmjs.com/package/reach-router-scroll-top
Related
I'm trying to setup a multi-level browsing pattern with React Router 6.3.0 on React 18.2.0. The pattern I'm trying to get to allows for drilling down into locations with nested paths such as:
/explore
/explore/USA
/explore/USA/Florida
The same component is able to process any of the routes by taking everything beyond the base as additional parameters. The routes are setup like this:
<BrowserRouter>
<Routes>
<Route path='/explore'>
<Route index element={<ExplorePath />} />
<Route path=':p1' element={<ExplorePath />} />
<Route path=':p1/:p2' element={<ExplorePath />} />
<Route path=':p1/:p2/:p3' element={<ExplorePath />} />
<Route path=':p1/:p2/:p3/:p4' element={<ExplorePath />} />
<Route path=':p1/:p2/:p3/:p4/:p5' element={<ExplorePath />} />
<Route path=':p1/:p2/:p3/:p4/:p5/:p6' element={<ExplorePath />} />
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
Sounds simple. Drilling down works fine. As I go to each deeper level the content renders as expected. The problem starts when trying to move up the train. I have breadcrumbs setup with the <Link> compontent. If I click on one for a higher level (such as "/explore/USA" when I'm looking at "/explore/USA/Florida") the URL updates as expected but the content does not update at all.
The only workaround I've found so far is to add the reloadDocument to my <Link> tags. This works, but it forces a page reload every time I navigate.
Is there a way to configure the routes to make the component re-render without forcing a full page reload?
Update 21-Jul-2022:
It isn't a fix, but I did find a workaround. Digging deeper, this seems to be an issue with how a React function and a React component work. First off, extracting route parameters in a component appears to be very messy (if possible at all). To get around it I created the following React function:
export function ExplorePath() {
const { p1, p2, p3, p4, p5, p6 } = useParams();
const parts = [p1, p2, p3, p4, p5, p6];
const trimmed = parts.filter(o => o !== undefined);
const path = trimmed.join('/');
return (
<Explore path={path} />
);
}
The function is being called any time I click a link that calls one of the defined routes. However, it appears that when the function calls the component it is reusing the component when navigating to a shorter path instead of creating a new one. As I drill down the tree I always end up with a new component. The workaround I ended up with is to check for a change in the props with componentDidUpdate like this:
constructor(props) {
super(props);
this.state = { isLoading: true, model: null };
}
componentDidMount() {
this.populateModel();
}
componentDidUpdate(prevProps: PageProps) {
if (this.props.path !== prevProps.path) {
this.setState({ isLoading: true, model: null });
this.populateModel();
}
}
So, did I find a bug in how React functions and components interact or is this just a quirk that I'm not understanding?
I have React application which has a structure similar to the following.
class App extends Component {
render() {
return (
<div className="App">
<NavBar />
<Router>
<Switch>
<Route path="/login" component={LoginPage} />
<Route path="/" exact component={DashboardPage} />
<Route path="/admin" exact component={AdminPage} />
// many other routes
<Route component={NotFound} />
</Switch>
</Router>
</div>
);
}
}
I do not want the login page to display the <NavBar /> element. I tried using the sessionStorage to get the userId and only display the navigation if the value is set. When I do this and go to the login page and the nav bar is not there. But when I log in, it's still not there. If I refresh however, it will appear.
I know one way to solve this is to make some sort of wrapper around the pages that do want the navigation, but I'd rather not have all of that code duplication, etc.
I feel this must be a common want, and I'm missing something dumb. Any help would be appreciated. I'm new to React so I don't follow everything that's going on here. Thanks.
I think your way of conditionally showing the NavBar is the right way. The question is how to trigger a state change so that the render method takes care of hiding and showing the NavBar, when you log in and out. I suggested maintaining a isLoggedIn state in your App component, and rendering the NavBar based on that, instead of directly accessing the SessionStorage. You could then use a custom event to update the state, when SessionStorage changes.
See this question for updating state based on Storage (in short, you fire and handle a custom event for storage changes): How to listen to localstorage in react.js
This might still be more code that you had hoped for, but it's more aligned with how React works, to derive the view (render) from component state.
I'm using the dynamic import() syntax to split each route component into a separate bundle and React.lazy to load the components only when they are required. I have a PageLoading component which serves as the fallback content for <React.Suspense />.
Is it possible to keep the current view component mounted until the "lazy loaded" component is ready to be rendered? The bundles are quite small individually so inevitably the page just ends up flashing for less than half a second while the "fallback" is rendered.
Thank you.
Edit: I am not looking to create an artificial delay. What am I asking for is exactly as I have described.
I wrote a component that accepts the lazy component you want to render and sets the fallback property of <Suspense /> to be the previously rendered component:
import * as React from 'react';
export type LazyProps = {
component: React.LazyExoticComponent<() => JSX.Element>;
initialFallback?: JSX.Element;
};
export const Lazy = ({ component, initialFallback = <></> }: LazyProps): JSX.Element => {
const fallback = React.useRef(() => initialFallback);
const Component = component;
const updateFallback = async (): Promise<void> => {
const result = await component._result;
fallback.current = typeof result === 'function' ? result : (result as any).default;
};
React.useEffect(() => {
updateFallback();
}, [component]);
return (
<React.Suspense fallback={<fallback.current />}>
<Component />
</React.Suspense>
);
};
Which can be used as such:
<Lazy component={MyLazyComponent} />
Do you mean something like this?
I was also searching for the right approach and tried many ways. Currently my best approach is to update the fallback whenever a page is rendered whether that page is lazy-loaded or not. So the fallback must be a state instead of something like
<div>Loading...</div>
It must be the same component instance as the currently rendered page.
Here is the source code for it.
Is it possible to keep the current view component mounted until the "lazy loaded" component is ready to be rendered?
Yes, my answer to a different question also applies here. Without seeing a code example, I don't know your exact situation - but you can achieve this behavior by simply wrapping your entire router Switch in Suspense.
For example:
<Suspense fallback={<PageLoading />}>
<Switch>
<Route exact path="/" component={Page1} />
<Route path="/page-2" component={Page2} />
<Route path="/page-3" component={Page3} />
</Switch>
</Suspense>
This is a basic situation: I have a Nav with links and several routes for those links in a main content area.
const App = () => (
<div id="page-container">
<Nav />
<main id="main">
<Switch>
<Route exact path="/" component={IndexPage} />
<Route exact path="/blog" component={BlogPage} />
<Route exact path="/about" component={AboutPage} />
<Route component={NotFoundPage} />
</Switch>
</main>
</div>
);
Sometimes when I click the nav links the main content area updates instantaneously, and sometimes it takes maybe 2 or 3 seconds to load the next component. I believe that's an outrageous amount of time to wait (with no indication that anything is loading).
All I seemed to find was React Router Transitions, which I tried, but it seemed like it required a static timeout for the transition animation. I only want a loading animation to appear where necessary; I do not want the routes animated every single time. React Transition Group also seems to require each route to be wrapped in a Transition component, which requires a location argument, which I don't seem able to have in my current layout.
Here's what I hoped to do:
I hoped to add a boolean state variable to my Page component to keep track of loading:
class Page extends React.Component {
constructor (props) {
super(props);
this.state = {
loading: true
};
}
componentDidMount() {
this.setState({
loading: false
});
}
render() {
return (
<article className={this.state.loading ? 'loading' : ''}>
... stuff ...
</article>
);
}
}
But this doesn't seem to work, because componentDidMount fires after render, i.e. after all of the loading time. So, basically, (1) it loads for awhile, (2) renders article with the 'loading' class for probably a few milliseconds until (3) componentDidMount sets the state and renders again, replacing the class.
So I'm guessing I need to add the loading animation on the previous page component, instead of the next page component? Or is it better to ajax all my page content? I'm really surprised how little information I've found on this, and I'm hoping someone has some helpful advice. Thanks!
Try using React Lazy and Suspense fallback
I have a React MaterialUI AppBarcomponent with property title , that I am changing based on the value returned by window.location.pathname. So as the page/url changes, the title will change with it. Looks something like below:
<AppBar
title={this.renderTitle()}
/>
renderTitle() {
if (window.location.pathname === '/home'
return 'home';
} else if (window.location.pathname === '/login'
return 'login';
}
The issue I am running into is that renderTitle() does not get executed if a different component (so not the AppBar) causes the page/url change.
E.g. another separate React component on the page triggers the page to change, which I'd hoped with trigger renderTitle(), but it doesn't... thus the title property never updates. So if I am navigating from /home to /login, the following will happen:
pathname is /home
user presses a button which runs a function, submit(), which is used to change the page w/ react-router
renderTitle() is run at this point, but window.location.pathname is still returning the previous page
submit() changes the page to /login
window.location.pathname is now correctly set to /login, but it is too late as renderTitle() has already been run
any help is appreciated, thanks
The best way is to use react-document-title library.
From documentation:
react-document-title provides a declarative way to specify document.title in a single-page app.
This component can be used on server side as well.
If your component that renders AppBar is an actual route you can just read the pathname from the props that react router injects.
<Router>
<Route path="/" component={App}>
<Route path="about" component={About} />
<Route path="inbox" component={Inbox}>
<Route path="messages/:id" component={Message} />
</Route>
</Route>
</Router>
For example in App, About, Inbox and Message you have access to the router props. And you can also pass the props to their children.
render() {
return (
<AppBar
title={this.renderTitle(this.props.location.pathname)}
/>
);
}
And in your function just use the parameter to return the correct result. Now because you are using props your component will update automatically when they change.