When I am entering one page of the app I pass data through location state using react-router. Then I access it via location.state.myDataObject. When I refresh the page it is still there, while I would like it to be empty. Here's what I've try to use:
const resetLocation = () => {
history.replace({
...location,
state: undefined,
});
};
useEffect(() => {
window.addEventListener('onbeforeunload', resetLocation);
}, []);
Or adding it to unmount action within useEffect but I guess it is not called when refreshing the page:
useEffect(() => {
return function cleanLocationState() {
history.replace({
...this.props.location,
state: undefined,
});
};
}, []);
I think this is the desired behavior of react router. If you want to reset the state then you need to do something like
import React, { useEffect, useCallback } from "react";
import { useLocation, useHistory } from "react-router-dom";
function Home() {
const location = useLocation();
const history = useHistory();
const replaceHistory = useCallback(() => {
history.replace({ ...location, state: undefined });
}, [history]);
useEffect(() => {
window.addEventListener("beforeunload", () => replaceHistory);
return () => {
window.removeEventListener("beforeunload", replaceHistory);
};
}, []);
return (
<div>
<h2>Home</h2>
</div>
);
}
export default Home;
Working example
How about you try the contrary? Store the value on component did mound and delete it from the location. I'm not sure that this is the prettiest solution, but i guess it's the easiest
const [state,setState]=useState();
useEffect(()=>{
setState(location.state);
location.state=undefined;
}, [location])
Try this way:
import React, { useEffect } from "react";
import { useHistory } from "react-router-dom";
function Home() {
const history = useHistory();
function replaceHistory(e) {
if (e) {
e.preventDefault();
delete e.returnValue;
}
history.replace({ ...history.location, state: undefined });
}
console.log("history", history.location);
useEffect(() => {
window.addEventListener("beforeunload", () => replaceHistory);
return () => {
// Reset Location state if we leave this page
replaceHistory();
window.removeEventListener("beforeunload", replaceHistory);
};
}, []);
return (
<div>
<h2>Home</h2>
</div>
);
}
export default Home;
CodesandBox Demo
The default behavior of the react-router will not save the history state after refresh the page so we need to know more about your code to really solve this issue. However, if the state do save, byour first attempt seem to have some flaw by using the history and location of window instead from the props.
function Page(props){
useEffect(() => {
const unloadFunc = () => {
//use the history and the location from the props instead of window
props.history.replace({
...props.location,
state: undefined,
});
}
window.addEventListener('onbeforeunload',unloadFunc);
return ()=>{
window.removeEventListener('onbeforeunload' unloadFunc);
//make history and location as the dependencies of the hook
}, [props.history, props.location]);
return <div></div>
}
Related
I would like to create a "go back" button that only goes back one page if the page is within the site.
I've tried following this answer to add a return button:
import { useNavigate } from 'react-router-dom';
function YourApp() {
const navigate = useNavigate();
return (
<>
<button onClick={() => navigate(-1)}>go back</button>
</>
);
}
But it goes one page back even if the page is not within the site.
E.g.: if I open a tab, go to stackoverflow.com, then go to my page and click the "go back" button, I will come back to StackOverflow.
How can I make it send me to a default page if the previous page is not within the site?
I ended up solving this by creating a history array in Redux and feeding it the current location from useLocation whenever it changes:
export const MyRoutes = () => {
const location = useLocation();
const dispatch = useDispatch();
useEffect(() => {
dispatch(pushLocationToHistory(location));
}, [location]);
};
And then I can just use the path of the last item if available.
The basic gist is that you can keep and maintain your own history stack and check if there are history entries to go back to. If the stack is empty then redirect to the app's home page, otherwise, allow the normal navigation.
Example using a React Context to hold the stack state and provide a customized navigate function.
import {
createContext,
useCallback,
useContext,
useEffect,
useState
} from "react";
import {
useNavigate as useNavigateBase,
UNSAFE_NavigationContext,
NavigationType
} from "react-router-dom";
const NavigateContext = createContext({
navigate: () => {},
});
const useNavigate = () => useContext(NavigateContext);
const NavigateProvider = ({ children }) => {
const [historyStack, setHistoryStack] = useState([]);
const navigateBase = useNavigateBase();
const { navigator } = useContext(UNSAFE_NavigationContext);
useEffect(() => {
const listener = ({ location, action }) => {
switch (action) {
case NavigationType.Push:
return setHistoryStack((stack) => stack.concat(location));
case NavigationType.Replace:
return setHistoryStack((stack) =>
stack.slice(0, -1).concat(location)
);
case NavigationType.Pop:
return setHistoryStack((stack) => stack.slice(0, -1));
default:
// ignore
}
};
return navigator.listen(listener);
}, [navigator]);
useEffect(() => {
console.log({ historyStack });
}, [historyStack]);
const navigate = useCallback(
(arg, options) => {
if (typeof arg === "number" && arg < 0 && !historyStack.length) {
navigateBase("/", { replace: true });
} else {
navigateBase(arg, options);
}
},
[historyStack, navigateBase]
);
return (
<NavigateContext.Provider value={navigate}>
{children}
</NavigateContext.Provider>
);
};
Example Usage:
const GoBackButton = () => {
const navigate = useNavigate();
return <button onClick={() => navigate(-1)}>go back</button>
}
...
function App() {
return (
<NavigateProvider>
... app code ...
</NavigateProvider>
);
}
I'm going crazy, I'm using react-router-dom, the moment I go from page A to B I want it to start at the top of the page, everywhere. I've tried different things like:
componentDidUpdate(){
console.log('hello');
document.documentElement.scrollTo(0, 0)
}
&
componentDidUpdate(){
console.log('hello');
window.scrollTo(0, 0)
}
&
import { useEffect } from 'react';
import { withRouter } from 'react-router-dom';
function ScrollToTop({ history }) {
useEffect(() => {
const unlisten = history.listen(() => {
window.scrollTo(0, 0);
});
return() => {
unlist();
}
}, []);
return(null);
}
export default withRouter(ScrollToTop);
But unfortunately without success, is there anyone who can tell me what I can do? I'm using :react": "^16.14.0"
So the intention is if I click on a <Link to={'../shopping cart'}>Shopping cart</Link> I will end up at the top of the page!
maybe you can use useRef hook. something like this:
const linkRef = useRef(null);
useEffect(() => {
if (linkRef.current) {
linkRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, []);
return <Link ref={linkRef} to='/shopping-card' />
How do we detect a change in the URL hash of a Next.js project?
I don't want to reload my page every time the slug changes.
I cannot use <Link> since all of my data comes from DB
Example:
When clicking on an tag from
http://example/test#url1
to
http://example.com/test#url2
Tried the below, but this seems to work for path change only.
import React, { useEffect,useState } from 'react';
import { useRouter } from 'next/router'
const test = () => {
const router = useRouter();
useEffect(() => {
console.log(router.asPath);
}, [router.asPath]);
return (<></>);
};
export default test;
You can listen to hash changes using hashChangeStart event from router.events.
const Test = () => {
const router = useRouter();
useEffect(() => {
const onHashChangeStart = (url) => {
console.log(`Path changing to ${url}`);
};
router.events.on("hashChangeStart", onHashChangeStart);
return () => {
router.events.off("hashChangeStart", onHashChangeStart);
};
}, [router.events]);
return (
<>
<Link href="/#some-hash">
<a>Link to #some-hash</a>
</Link>
<Link href="/#some-other-hash">
<a>Link to #some-other-hash</a>
</Link>
</>
);
};
If you're not using next/link or next/router for client-side navigation (not recommended in Next.js apps), then you'll need to listen to the window's hashchange event.
Your useEffect would look like the following.
useEffect(() => {
const onHashChanged = () => {
console.log('Hash changed');
};
window.addEventListener("hashchange", onHashChanged);
return () => {
window.removeEventListener("hashchange", onHashChanged);
};
}, []);
If you're relying on URL hash for multiple re-renders or state changes, note that NextJS hashChangeStart event does not account for browser refresh or direct browser URL address navigation
A complete solution might need a combination of event listeners to cover all edge cases.
const useUrlHash = (initialValue) => {
const router = useRouter()
const [hash, setHash] = useState(initialValue)
const updateHash = (str) => {
if (!str) return
setHash(str.split('#')[1])
}
useEffect(() => {
const onWindowHashChange = () => updateHash(window.location.hash)
const onNextJSHashChange = (url) => updateHash(url)
router.events.on('hashChangeStart', onNextJSHashChange)
window.addEventListener('hashchange', onWindowHashChange)
window.addEventListener('load', onWindowHashChange)
return () => {
router.events.off('hashChangeStart', onNextJSHashChange)
window.removeEventListener('load', onWindowHashChange)
window.removeEventListener('hashchange', onWindowHashChange)
}
}, [router.asPath, router.events])
return hash
}
Problem with removing addEventListener using useEffect return
I also tried to add empty array as a second argument to an useEffect, listener still remains
import React, { useCallback, useEffect } from "react";
import headerLogo from "./icons/logo.svg";
import { useHistory } from "react-router-dom";
function Home({ header }) {
const history = useHistory();
useEffect(() => {
const mouseClickListener = (e) => {
e.preventDefault();
console.log("LISTENING");
history.push("/dashboard");
};
window.addEventListener("click", mouseClickListener);
return () => window.removeEventListener("click", mouseClickListener);
});
return (
<div className={`${header ? "header-logo" : "home-logo"}`}>
<img src={headerLogo} alt="logo" />
</div>
);
}
export default Home;
That was all about, routing. Home component was rendering in another routes also and useEffect was setting event listener each time.
what I did was adding condition by wrapping listener functionality inside useEffect
useEffect(() => {
if (!header) {
const mouseClickListener = (e) => {
e.preventDefault();
console.log("LISTENING");
history.push("/dashboard");
};
window.addEventListener("click", mouseClickListener);
return () => window.removeEventListener("click", mouseClickListener);
}
});
I have 2 screens, on my first screen I have a useEffect which has a watcher on a redux state.
Screen 1
...
useEffect(() => {
if (props.data) {
navigation.navigate('To Other Screen');
}
}, [props.data]);
...
Then on my second Screen.
I have a function that will update the redux 'data'.
Then, the useEffect on the previous was triggered.
How will I prevent this one.
Thank you.
I found this hoc on the react-nativgation and combine this with useEffect;
https://reactnavigation.org/docs/1.x/with-navigation-focus/
import { withNavigationFocus } from 'react-navigation';
...
const Page = (props) => {
useEffect(() => {
if (props.isFocused) {
// function call;
}
}, [props.isFocused]);
};
export default withNavigationFocus(Page);
Try this,
React.useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
if(props.data){
navigation.navigate('To Other Screen')
}
})
return unsubscribe
}, [navigation, props.data])