Reputation: 2415
Why useEffect doesn't run on window.location.pathname
changes? I get loc
logged only once.
How can I make to run useEffect when pathname changes without any additional libraries?
useEffect(() => {
const loc = window.location.pathname
console.log({ loc })
}, [window.location.pathname])
Upvotes: 30
Views: 34477
Reputation: 458
Weird no one mentioned this but, you can get location from react-router-dom
using the useLocation
hook. So you can just use that in the dependency array.
Docs here
const location = useLocation();
useEffect(() => {
console.log(location);
}, [location.pathname]);
Edit:
This is a solution only for using react with web and you are using the react-router-dom
library for web.
If you want to achieve this for cases where you don't have that library installed, and the other answers didn't work for you, you would need to do the following:-
export const LocationContext = React.createContext<LocationContextObject>(
null!
);
Set the value of this context provider with the location
When navigating, you need to update this context value with the new location, (create a custom navigation function and use it everywhere)
** Inspired from the source code of remix-run/react-router (react-router-dom)
{1} - You may want to search a bit more if you want to use a context at the top level since there are pitfalls you need to keep in mind. You could also store it in a state management library of your choice.
Upvotes: 26
Reputation: 37
I don't know why but for me adding listeners for 'popstate'
never worked and I was able to get useEffect
to change when window.location.pathname
changes, similar to what karolis did in their original question without issue. This is what I did:
let path = window.location.pathname;
/* potentially changes navbar on page change */
useEffect(() => {
if (window.location.pathname === "/") {
setScrollNav(false);
} else {
setScrollNav(true);
}
}, [path]);
This seemed to solve the problem I was having, but it seems I had a very different experience compared to everyone else so I would love to know your thoughts.
Upvotes: 2
Reputation: 9807
useEffect
is evaluated every time your component renders. To subscribe to changes to location.pathname
, you'll need to add a listener to the window
's 'popstate'
event that updates state, which tells the component tree to rerender.
Rafel Mora's answer implements a hook using setState
, which will cause the component to rerender. You can then use the returned state value from the hook in your useEffect
in place of window.location.pathname
.
Related - here's the ESLint warning you'll see if you use eslint-plugin-react-hooks
:
Outer scope values like 'window.location.pathname' aren't valid dependencies because mutating them doesn't re-render the component
If you're open to using a library, React Router offers a useLocation hook.
Upvotes: 4
Reputation: 1109
I adapted Rafael Mora's answer to work for the entire location object and also work in the front end of Next.js apps using the useIsMounted
approach, and added typescript types.
hooks/useWindowLocation.ts
import useIsMounted from './useIsMounted'
import { useEffect, useState } from 'react'
const useWindowLocation = (): Location|void => {
const isMounted = useIsMounted()
const [location, setLocation] = useState<Location|void>(isMounted ? window.location : undefined)
useEffect(() => {
if (!isMounted) return
const setWindowLocation = () => {
setLocation(window.location)
}
if (!location) {
setWindowLocation()
}
window.addEventListener('popstate', setWindowLocation)
return () => {
window.removeEventListener('popstate', setWindowLocation)
}
}, [isMounted, location])
return location
}
export default useWindowLocation
hooks/useIsMounted.ts
import { useState, useEffect } from 'react'
const useIsMounted = (): boolean => {
const [isMounted, setIsMounted] = useState(false)
useEffect(() => {
setIsMounted(() => true)
}, [])
return isMounted
}
export default useIsMounted
Upvotes: 5
Reputation: 1221
Create a hook, something like:
const useReactPath = () => {
const [path, setPath] = React.useState(window.location.pathname);
const listenToPopstate = () => {
const winPath = window.location.pathname;
setPath(winPath);
};
React.useEffect(() => {
window.addEventListener("popstate", listenToPopstate);
return () => {
window.removeEventListener("popstate", listenToPopstate);
};
}, []);
return path;
};
Then in your component use it like this:
const path = useReactPath();
React.useEffect(() => {
// do something when path changes ...
}, [path]);
Of course you'll have to do this in a top component.
Upvotes: 27