karolis2017
karolis2017

Reputation: 2415

Why useEffect doesn't run on window.location.pathname changes?

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

Answers (5)

Scramjet
Scramjet

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:-

  1. Add a context{1} to the top level of your App.
export const LocationContext = React.createContext<LocationContextObject>(
  null!
); 
  1. Set the value of this context provider with the location

  2. 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

PrettyMuchDone
PrettyMuchDone

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

JBallin
JBallin

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

animatedgif
animatedgif

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

Rafael Mora
Rafael Mora

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

Related Questions