It's not an uncommon peeve for a single-page app in React to make browsers refresh upon clicking on a link, regardless of where it leads. This happens often in WordPress themes made with React, and many MERN stack apps as well. Contrary to many of the answers I've come across, there seems to be a workaround, assuming you're using React Router and the React Hooks API.
My solution involves preventing the default behavior of anchor tags and pushing to the browser history object, therefore access to the history object needs to be provided to the tags. My initial solution involved prop-drilling, the most common solution to which is to use a context, as follows:
import React, { useState, createContext } from 'react';
let initialState = ({ history }) => ({ history });
const GeneralContext = createContext(initialState);
export default GeneralContext;
const { Provider } = GeneralContext;
export const GeneralContextProvider = ({ children, initialVals }) => {
let iState = Object.assign({}, initialState(initialVals)), [generalState, setGeneralState] = useState(iState);
return <Provider value={{ generalState, setGeneralState }}>{children}</Provider>;
};
Wherever in the code you specify your routes, create a functional component that returns a Route
with the GeneralContextProvider
and the component nested inside, then refactor accordingly:
import React from 'react';
import { Route } from 'react-router-dom';
let GeneralRoute = ({ component: Component, ...rest }) => <Route exact {...rest} component={({ history }) => (
<GeneralContextProvider initialVals={{ history, match }}>
<Component /></GeneralContextProvider>
)} />;
As you can see, the GeneralContext
is provided the browser history object by means of the Route
component attribute. Anything in this attribute will be passed an object with a location
, history
, and match
attributes.
Now, for the actual link itself (don't mind the use of styled-components
):
import React, { useContext } from 'react';
import GeneralContext from './GeneralContext';
function SamePageAnchor({ children, href, target, className, id, style, component }) {
let { generalState, setGeneralState } = useContext(GeneralContext), Anchor = component || styled.a"", AlreadyOn = styled.span" text-decoration: underline; font-weight: 900; margin: 0; width: fit-content; height: fit-content;";
function handleClick(event) {
if (href.startsWith('/')) {
let newState = Object.assign({}, generalState);
event.preventDefault();
newState.history.push(href);
setGeneralState(newState);
}
}
return (generalState.history && generalState.history.location.pathname !== href) ? <Anchor {...{ href, target, className, id, style }} onClick={handleClick}>{children}</Anchor> : <AlreadyOn>{children}</AlreadyOn>;
}
export default SamePageAnchor;
Now you should be good to go. Make sure that wherever in your code used, it has access to a GeneralContext
.