How can I use react-router, and have a link navigate to a particular place on a particular page? (e.g. /home-page#section-three)
Details:
I am using react-router in my React app.
I have a site-wide navbar that needs to link to a particular parts of a page, like /home-page#section-three.
So even if you are on say /blog, clicking this link will still load the home page, with section-three scrolled into view. This is exactly how a standard <a href="/home-page#section-three> would work.
Note: The creators of react-router have not given an explicit answer. They say it is in progress, and in the mean time use other people's answers. I'll do my best to keep this question updated with progress & possible solutions until a dominant one emerges.
Research:
How to use normal anchor links with react-router
This question is from 2015 (so 10 years ago in react time). The most upvoted answer says to use HistoryLocation instead of HashLocation. Basically that means store the location in the window history, instead of in the hash fragment.
Bad news is... even using HistoryLocation (what most tutorials and docs say to do in 2016), anchor tags still don't work.
https://github.com/ReactTraining/react-router/issues/394
A thread on ReactTraining about how use anchor links with react-router. This is no confirmed answer. Be careful since most proposed answers are out of date (e.g. using the "hash" prop in <Link>)
React Router Hash Link worked for me and is easy to install and implement:
$ npm install --save react-router-hash-link
In your component.js import it as Link:
import { HashLink as Link } from 'react-router-hash-link';
And instead of using an anchor <a>, use <Link> :
<Link to="home-page#section-three">Section three</Link>
Note: I used HashRouter instead of Router:
This solution works with react-router v5
import React, { useEffect } from 'react'
import { Route, Switch, useLocation } from 'react-router-dom'
export default function App() {
const { pathname, hash, key } = useLocation();
useEffect(() => {
// if not a hash link, scroll to top
if (hash === '') {
window.scrollTo(0, 0);
}
// else scroll to id
else {
setTimeout(() => {
const id = hash.replace('#', '');
const element = document.getElementById(id);
if (element) {
element.scrollIntoView();
}
}, 0);
}
}, [pathname, hash, key]); // do this on route change
return (
<Switch>
<Route exact path="/" component={Home} />
.
.
</Switch>
)
}
In the component
<Link to="/#home"> Home </Link>
Here is one solution I have found (October 2016). It is is cross-browser compatible (tested in Internet Explorer, Firefox, Chrome, mobile Safari, and Safari).
You can provide an onUpdate property to your Router. This is called any time a route updates. This solution uses the onUpdate property to check if there is a DOM element that matches the hash, and then scrolls to it after the route transition is complete.
You must be using browserHistory and not hashHistory.
The answer is by "Rafrax" in Hash links #394.
Add this code to the place where you define <Router>:
import React from 'react';
import { render } from 'react-dom';
import { Router, Route, browserHistory } from 'react-router';
const routes = (
// your routes
);
function hashLinkScroll() {
const { hash } = window.location;
if (hash !== '') {
// Push onto callback queue so it runs after the DOM is updated,
// this is required when navigating from a different page so that
// the element is rendered on the page before trying to getElementById.
setTimeout(() => {
const id = hash.replace('#', '');
const element = document.getElementById(id);
if (element) element.scrollIntoView();
}, 0);
}
}
render(
<Router
history={browserHistory}
routes={routes}
onUpdate={hashLinkScroll}
/>,
document.getElementById('root')
)
If you are feeling lazy and don't want to copy that code, you can use Anchorate which just defines that function for you. https://github.com/adjohnson916/anchorate
Here's a simple solution that doesn't require any subscriptions nor third-party packages. It should work with react-router#3 and above and react-router-dom.
Working example: https://fglet.codesandbox.io/
Source (unfortunately, it doesn't currently work within the editor):
#ScrollHandler Hook Example
import { useEffect } from "react";
import PropTypes from "prop-types";
import { withRouter } from "react-router-dom";
const ScrollHandler = ({ location, children }) => {
useEffect(
() => {
const element = document.getElementById(location.hash.replace("#", ""));
setTimeout(() => {
window.scrollTo({
behavior: element ? "smooth" : "auto",
top: element ? element.offsetTop : 0
});
}, 100);
}, [location]);
);
return children;
};
ScrollHandler.propTypes = {
children: PropTypes.node.isRequired,
location: PropTypes.shape({
hash: PropTypes.string,
}).isRequired
};
export default withRouter(ScrollHandler);
#ScrollHandler Class Example
import { PureComponent } from "react";
import PropTypes from "prop-types";
import { withRouter } from "react-router-dom";
class ScrollHandler extends PureComponent {
componentDidMount = () => this.handleScroll();
componentDidUpdate = prevProps => {
const { location: { pathname, hash } } = this.props;
if (
pathname !== prevProps.location.pathname ||
hash !== prevProps.location.hash
) {
this.handleScroll();
}
};
handleScroll = () => {
const { location: { hash } } = this.props;
const element = document.getElementById(hash.replace("#", ""));
setTimeout(() => {
window.scrollTo({
behavior: element ? "smooth" : "auto",
top: element ? element.offsetTop : 0
});
}, 100);
};
render = () => this.props.children;
};
ScrollHandler.propTypes = {
children: PropTypes.node.isRequired,
location: PropTypes.shape({
hash: PropTypes.string,
pathname: PropTypes.string,
})
};
export default withRouter(ScrollHandler);
Just avoid using react-router for local scrolling:
document.getElementById('myElementSomewhere').scrollIntoView()
The problem with Don P's answer is sometimes the element with the id is still been rendered or loaded if that section depends on some async action. The following function will try to find the element by id and navigate to it and retry every 100 ms until it reaches a maximum of 50 retries:
scrollToLocation = () => {
const { hash } = window.location;
if (hash !== '') {
let retries = 0;
const id = hash.replace('#', '');
const scroll = () => {
retries += 0;
if (retries > 50) return;
const element = document.getElementById(id);
if (element) {
setTimeout(() => element.scrollIntoView(), 0);
} else {
setTimeout(scroll, 100);
}
};
scroll();
}
}
I adapted Don P's solution (see above) to react-router 4 (Jan 2019) because there is no onUpdate prop on <Router> any more.
import React from 'react';
import * as ReactDOM from 'react-dom';
import { Router, Route } from 'react-router';
import { createBrowserHistory } from 'history';
const browserHistory = createBrowserHistory();
browserHistory.listen(location => {
const { hash } = location;
if (hash !== '') {
// Push onto callback queue so it runs after the DOM is updated,
// this is required when navigating from a different page so that
// the element is rendered on the page before trying to getElementById.
setTimeout(
() => {
const id = hash.replace('#', '');
const element = document.getElementById(id);
if (element) {
element.scrollIntoView();
}
},
0
);
}
});
ReactDOM.render(
<Router history={browserHistory}>
// insert your routes here...
/>,
document.getElementById('root')
)
<Link to='/homepage#faq-1'>Question 1</Link>
useEffect(() => {
const hash = props.history.location.hash
if (hash && document.getElementById(hash.substr(1))) {
// Check if there is a hash and if an element with that id exists
document.getElementById(hash.substr(1)).scrollIntoView({behavior: "smooth"})
}
}, [props.history.location.hash]) // Fires when component mounts and every time hash changes
For simple in-page navigation you could add something like this, though it doesn't handle initializing the page -
// handle back/fwd buttons
function hashHandler() {
const id = window.location.hash.slice(1) // remove leading '#'
const el = document.getElementById(id)
if (el) {
el.scrollIntoView()
}
}
window.addEventListener('hashchange', hashHandler, false)
An alternative: react-scrollchor https://www.npmjs.com/package/react-scrollchor
react-scrollchor: A React component for scroll to #hash links with smooth animations. Scrollchor is a mix of Scroll and Anchor
Note: It doesn't use react-router
Create A scrollHandle component
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
export const ScrollHandler = ({ children}) => {
const { pathname, hash } = useLocation()
const handleScroll = () => {
const element = document.getElementById(hash.replace("#", ""));
setTimeout(() => {
window.scrollTo({
behavior: element ? "smooth" : "auto",
top: element ? element.offsetTop : 0
});
}, 100);
};
useEffect(() => {
handleScroll()
}, [pathname, hash])
return children
}
Import ScrollHandler component directly into your app.js file
or you can create a higher order component withScrollHandler and export your app as withScrollHandler(App)
And in links <Link to='/page#section'>Section</Link> or <Link to='#section'>Section</Link>
And add id="section" in your section component
I know it's old but in my latest react-router-dom#6.4.4, this simple attribute reloadDocument is working:
div>
<Link to="#result" reloadDocument>GO TO ⬇ (Navigate to Same Page) </Link>
</div>
<div id='result'>CLICK 'GO TO' ABOVE TO REACH HERE</div>
Related
Is there a built-in way in react-router-dom v6, to go back to the previous page, BUT in case the previous page is out of the context of the application, to route to the root and to thus not out of the application.
Example: I surf to a www.thing.com/thingy from www.google.com, this page (www.thing.com/thingy) has a go back button on it => when I click on the go back button => I am redirected to www.google.com instead of the wanted behaviour a redirect to www.thing.com.
Mockup of an example page.
I have tried several implementations and searched through the documentation but couldn't find a built-in way to resolve this. As far as I can see there isn't a way. I can however make something custom to resolve my issue if its not.
import { useNavigate } from 'react-router-dom';
function YourApp() {
const navigate = useNavigate();
return (
<>
<button onClick={() => navigate(-1)}>go back</button>
</>
);
}
I solved it by keeping track of the history.
If a user had not yet been on the page, I redirect them to the homepage.
Else redirect them to the previous page.
import {
useEffect
} from 'react';
import {
createContext,
useMemo,
useState
} from 'react';
import {
useLocation
} from 'react-router-dom';
export const LocationHistoryContext = createContext({});
const LocationHistoryProvider = ({
children
}) => {
const [locationHistory, setLocationHistory] = useState(
new Set(),
);
const location = useLocation();
useEffect(() => {
// if pathname has changed, add it to the history
let path = location.pathname.match(/^\/([^/])*/)[0];
setLocationHistory((prev) => new Set([path, ...prev]));
}, [location.pathname, location]);
const context = useMemo(() => {
return {
/* if the user has visited more than one page */
hasHistory: locationHistory.size > 1,
};
}, [locationHistory]);
return ( <
LocationHistoryContext.Provider value = {context}>
{
children
}
</LocationHistoryContext.Provider>
);
};
export default LocationHistoryProvider;
How to scroll to top on route change with react router dom v6?
I have tried this, react-router scroll to top on every transition, which was my solution to make my page scroll to top on route change when I use react-router-dom v5. Now, I am using react-router-dom v6 and this solution does not work.
I tried React-router v6 window.scrollTo does not work and does not work for me.
I tried https://github.com/remix-run/react-router/issues/7365, which is to use the preload prop to trigger the scrollTo(0,0), also does not work for me.
Well I'm not really sure what your layout looks like but inside your <BrowserRouter /> you can wrap your app in a wrapper and check for the location change in a useLayoutEffect. Then if there is a change you can scroll to the top. Here is a crude example.
Codesandbox
import { BrowserRouter, Routes, Route, Link, useLocation } from 'react-router-dom'
import { useLayoutEffect } from 'react'
const Wrapper = ({children}) => {
const location = useLocation();
useLayoutEffect(() => {
document.documentElement.scrollTo(0, 0);
}, [location.pathname]);
return children
}
const Component = ({title}) => {
return (
<div>
<p style={{paddingTop: '150vh'}}>{title}</p>
</div>
)
}
const App = () => {
return (
<BrowserRouter>
<Wrapper>
<p>Scroll the bottom to change pages</p>
<Routes>
<Route path="/" element={<Component title="Home"/>} />
<Route path="/about" element={<Component title="About"/>} />
<Route path="/product" element={<Component title="Product"/>} />
</Routes>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/product">Product</Link>
</Wrapper>
</BrowserRouter>
)
}
export default App
this article resolves this issue See it -> https://www.matthewhoelter.com/2022/04/02/how-to-scroll-to-top-on-route-change-with-react-router-dom-v6.html
make ScrollToTop component
and then add this code init
import { useLocation } from "react-router-dom";
export default function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
// "document.documentElement.scrollTo" is the magic for React Router Dom v6
document.documentElement.scrollTo({
top: 0,
left: 0,
behavior: "instant", // Optional if you want to skip the scrolling animation
});
}, [pathname]);
return null;
}
and then import it in your App.js and now your issue is resolved
see img
const scrollToPosition = (top = 0) => {
try {
/**
* Latest API
*/
window.scroll({
top: top,
left: 0,
behavior: "smooth",
});
} catch (_) {
/**
* Fallback
*/
window.scrollTo(0, top);
}
};
You can use the above code to scroll top.
const didMount = useDidMount();
const router = useRouter();
const { asPath } = router;
useEffect(() => {
if (didMount) {
scrollToPosition();
}
}, [asPath]);
And add the above code to the top parent component.
The window function cannot be accessed in newer versions of react, it is better to use the useRef Hook.
const myRef = useRef<any>(null);
const executeScroll = () => myRef.current.scrollIntoView({inline: "center"});
useEffect(() => {
executeScroll();
}, [location.pathname]);
// YOUR COMPONENTS CODE
// I have my toolbar as the ref
<Toolbar ref={myRef}/>
This will scroll when using the useNavigator and the pathname changes.
I have a bottomTabNavigator which has two stacknavigators. Each stacknavigator has their own respective screens within them. Whenever I use something like
navigator.navigate("Stackname" {screen:"screenname", randomProp: "seomthing")
the params are sent to the stacknavigator, and not the screen itself. I kinda got past the issue by passing in
initialParams=route.params
within the stacknavigators, but they won't refresh when I call the first block of code for a second time.
Any ideas?
Instead of:
navigator.navigate("StackName" {screen:"screenName", paramPropKey: "paramPropValue");
Use this:
navigator.navigate("screenName", {'paramPropKey': 'paramPropValue'});
In screenName:
export default ({route}) => {
useEffect(() => {
// do something
}, [route]);
};
That is because the screen is already mounted & initial params won't update. What you can do, though, is create a wrapper component enhanced with 'withNavigationFocus' that 'react-native-navigation' offers.
https://reactnavigation.org/docs/1.x/with-navigation-focus/
ComponentWithFocus
import React, {Component, useState} from 'react';
import { withNavigationFocus } from 'react-navigation';
const ComponentWithFocus = (props) => {
const {isFocused, onFocus, onBlur} = props;
const [focused, setFocused] = useState(false);
if(isFocused !== focused) {
if(isFocused) {
typeof onFocus === 'function' ? onFocus() : null;
setFocused(true)
} else {
typeof onBlur === 'function' ? onBlur() : null;
setFocused(false)
}
}
return (
props.children
)
}
export default withNavigationFocus(ComponentWithFocus);
And use it in your screen like this:
...
onFocus = () => {
//your param fetch here and data get/set
this.props.navigation.getParam('param')
//get
//set
}
...
render() {
<ComponentWithFocus onFocus={this.onFocus}>
/// Your regular view JSX
</ComponentWithFocus>
}
Note: If params aren't updated still, than you should reconsider your navigating approach. For example, there is no need to navigate from your tabBar like this:
navigator.navigate("Stackname" {screen:"screenname", randomProp: "seomthing")
You could instead do the following:
navigator.navigate("screenName", {'paramPropKey': 'paramPropValue'})
This will work because '.navigate' function finds the first available screen that matches the name and if it isn't already mounted it mounts it onto the stack (firing componentDidMount method). If the screen already exists, it just navigates to it, ignoring 'componentDidMount' but passing the 'isFocused' prop which, luckily, we hooked on to in our 'ComponentWithFocus'.
Hope this helps.
function HomeScreenComponent( {navigation} ) {
React.useEffect(() => {
navigation.addListener('focus', () => {
console.log("reloaded");
});
}, [navigation]);
export default HomeScreenComponent;
This will also listen to the focusing and execute the useEffect function when the screen navigates.
How to scroll into view after routing in react
We are using react-router. What I want to achieve is do a scroll into view on one of component after react route to that page.
Here's a quick example for you using react-router-dom and refs. You didn't provide any code for us to look at so consider this a template. :)
Also here's a sandbox for your reference: https://codesandbox.io/s/adoring-surf-h55ci
So let's say your Routes are set-up like this:
Routes
function App() {
return (
<BrowserRouter>
<div>
<Route path="/" exact component={Home} />
<Route path="/example" component={Example} />
</div>
</BrowserRouter>
);
}
So user starts out at Home "/"
import React from "react";
import { Link } from "react-router-dom";
const Home = () => {
return <Link to="/example">Go To Example</Link>;
};
export default Home;
They click on the link and it takes them to the /example route, rendering Example
import React from "react";
import Section from "./Section";
class Example extends React.Component {
componentDidMount() {
if (this.mySection.current) {
this.mySection.current.scrollIntoView({
behavior: "smooth",
nearest: "block"
});
}
}
mySection = React.createRef();
render() {
return (
<div>
<div style={{ background: "orange", height: "750px" }}>
This is an example, below is my component.
</div>
<div ref={this.mySection}>
<Section />
</div>
</div>
);
}
}
export default Example;
Example has two parts, a div with plain text and the Section component. As you noticed, we wrap the Section component in a div and gave it a ref prop. The ref lets us communicate with that wrapper-div. In componentDidMount(), we just scroll to that div.
One way to scroll to a component would be to put a ref on the component you want to scroll to:
https://reactjs.org/docs/refs-and-the-dom.html
Once you have put the ref onto the component, you could then scroll to your ref in the componentDidMount() of the parent component, something like:
window.scrollTo(0, this.myRef.current.offsetTop)
You may need to be slightly more defensive here, and do something like this:
this.myRef && window.scrollTo(0, this.myRef.current.this.myRef)
This way, when the route is visited, the component will be scrolled to its offsetTop
All of these solution seem to be wrong
what expected is on click -> route -> scroll into a view
With above examples what will happen is on any render you scroll a particular element into a view...
The behaviour for general componentDidMount should stay as is ...
You can either add a hash to a routing to distinguish if it's a render from let's say page a - > then on page itself check if there is param in query.
Then the behaviour will be clean for componentDidMount.
For the above answers which i don't find correct you can use
useSmoothScroll(
target: string,
options: {
freezeStickyComponents?: boolean
hashLocation?: string
offset?: number | boolean
preventDefault?: boolean
} = {},
callback: (hash?: string) => void = DEFAULT_CALLBACK
): UseSmoothScrollCallback {
const defaultOptions = {
freezeStickyComponents: true,
hashLocation: null,
offset: false,
preventDefault: true
}
options = { ...defaultOptions, ...options }
const hasHash = options.hashLocation || target
const hash = hasHash && `${hasHash}`
const { freeze } = useStickyState()
return useCallback(
(event: SyntheticEvent<HTMLElement>): void => {
if (options.preventDefault) {
event.preventDefault()
}
if (!target) {
scrollToTop(callback)
return
}
const reference = document.getElementById(target)
if (typeof options.offset === 'number') {
window.scroll({
top:
reference?.getBoundingClientRect().top +
window.scrollY -
options.offset,
behavior: 'smooth'
})
} else {
reference?.scrollIntoView({
behavior: 'smooth'
})
}
window.history.pushState(null, null, hash)
callback(hash)
if (options.freezeStickyComponents) {
freeze(800)
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[freeze, hash, options, target]
)
}
I have a react component Data which includes several charts components; BarChart LineChart ...etc.
When Data component starts rendering, it takes a while till receiving the data required for each chart from APIs, then it starts to respond and render all the charts components.
What I need is, to start rendering each chart only when I scroll down and reach it on the page.
Is there any way could help me achieving this??
You have at least three options how to do that:
Track if component is in viewport (visible to user). And then render it. You can use this HOC https://github.com/roderickhsiao/react-in-viewport
Track ‘y’ scroll position explicitly with https://react-fns.netlify.com/docs/en/api.html#scroll
Write your own HOC using Intersection Observer API https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
To render component you may need another HOC, which will return Chart component or ‘null’ based on props it receives.
I have tried many libraries but couldn't find something that best suited my needs so i wrote a custom hook for that, I hope it helps
import { useState, useEffect } from "react";
const OPTIONS = {
root: null,
rootMargin: "0px 0px 0px 0px",
threshold: 0,
};
const useIsVisible = (elementRef) => {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
if (elementRef.current) {
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.unobserve(elementRef.current);
}
});
}, OPTIONS);
observer.observe(elementRef.current);
}
}, [elementRef]);
return isVisible;
};
export default useIsVisible;
and then you can use the hook as follows :
import React, { useRef } from "react";
import useVisible from "../../hooks/useIsVisible";
function Deals() {
const elemRef = useRef();
const isVisible = useVisible(elemRef);
return (
<div ref={elemRef}>hello {isVisible && console.log("visible")}</div>
)}
I think the easiest way to do this in React is using react-intersection-observer.
Example:
import { useInView } from 'react-intersection-observer';
const Component = () => {
const { ref, inView, entry } = useInView({
/* Optional options */
threshold: 0,
});
useEffect(()=>{
//do something here when inView is true
}, [inView])
return (
<div ref={ref}>
<h2>{`Header inside viewport ${inView}.`}</h2>
</div>
);
};
I also reccommend using triggerOnce: true in the options object so the effect only happens the first time the user scrolls to it.
you can check window scroll position and if the scroll position is near your div - show it.
To do that you can use simple react render conditions.
import React, {Component} from 'react';
import PropTypes from 'prop-types';
class MyComponent extends Component {
constructor(props){
super(props);
this.state = {
elementToScroll1: false,
elementToScroll2: false,
}
this.firstElement = React.createRef();
this.secondElement = React.createRef();
}
componentDidMount() {
window.addEventListener('scroll', this.handleScroll);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
}
handleScroll(e){
//check if scroll position is near to your elements and set state {elementToScroll1: true}
//check if scroll position is under to your elements and set state {elementToScroll1: false}
}
render() {
return (
<div>
<div ref={this.firstElement} className={`elementToScroll1`}>
{this.state.elementToScroll1 && <div>First element</div>}
</div>
<div ref={this.secondElement} className={`elementToScroll2`}>
{this.state.elementToScroll2 && <div>Second element</div>}
</div>
</div>
);
}
}
MyComponent.propTypes = {};
export default MyComponent;
this may help you, it's just a quick solution. It will generate you some rerender actions, so be aware.