React router v6 history.listen

Viewed 10528

In React Router v5 there was a listen mehtode on the history object. With the method I wrote a usePathname hook to rerender a component on a path change. In React Router v6, this method no longer exists. Is there an alternative for something like this? I would hate to use useLocation because it also renders if the state changes, which I don't need in this case.

The hook is used with v5.

import React from "react";
import { useHistory } from "react-router";

export function usePathname(): string {
  let [state, setState] = React.useState<string>(window.location.pathname);

  const history = useHistory();
  React.useLayoutEffect(
    () =>
      history.listen((locationListener) => setState(locationListener.pathname)),
    [history]
  );
  return state;
}
6 Answers

I am using now this code

import { BrowserHistory } from "history";
import React, { useContext } from "react";
import { UNSAFE_NavigationContext } from "react-router-dom";

export default function usePathname(): string {
  let [state, setState] = React.useState<string>(window.location.pathname);

  const navigation = useContext(UNSAFE_NavigationContext)
    .navigator as BrowserHistory;
  React.useLayoutEffect(() => {
    if (navigation) {
      navigation.listen((locationListener) =>
        setState(locationListener.location.pathname)
      );
    }
  }, [navigation]);

  return state;
}

It seems to work fine

As mentioned above, useLocation can be used to perform side effects whenever the current location changes.

Here's a simple typescript implementation of my location change "listener" hook. Should help you get the result you're looking for

function useLocationEffect(callback: (location?: Location) => any) {
  const location = useLocation();

  useEffect(() => {
    callback(location);
  }, [location, callback]);
}

// usage:
useLocationEffect((location: Location) => 
  console.log('changed to ' + location.pathname));

I find using useNavigate and useLocation quite meaningless compared to useHistory in React Rrouter v5.

As a result of these changes, I made a thin custom hook to ease myself from any refactoring.

Just rename the import path to this hook and use the "old" api with v6. To answer or just give hints to your question - using this approach is should be easy to implement the listen function in the custom hook yourself.

export function useHistory() {
  const navigate = useNavigate();
  const location = useLocation();

  const listen = ...; // implement the hook yourself

  return {
    push: navigate,
    go: navigate,
    goBack: () => navigate(-1),
    goForward: () => navigate(1),
    listen,
    location,
  };
}

Why not simply use const { pathname } = useLocation();? It will indeed renders if the state changes but it shouldn't be a big deal in most scenarii.

If you REALLY want to avoid such behaviour, you could create a context of your own to hold the pathname:

// PathnameProvider.js

import React, { createContext, useContext } from 'react';
import { useLocation } from 'react-router';

const PathnameContext = createContext();

const PathnameProvider = ({ children }) => {
  const { pathname } = useLocation();

  return (
    <PathnameContext.Provider value={pathname}>
      {children}
    </PathnameContext.Provider>
  );
}

const usePathname = () => useContext(PathnameContext);

export { PathnameProvider as default, usePathname };

Then you can use usePathname() in any component down the tree. It will render only if the pathname actually changed.

Given that @kryštof-Řeháček's recommendation (just above) is to implement your own useListen hook, but it might not be obvious how to do that, here's a version I've implemented for myself as a guide (nb: I havent't exhaustively unit tested this yet):

import { useState } from "react";
import { useLocation } from "react-router";

interface HistoryProps {
    index: number;
    isHistoricRoute: boolean;
    key: string;
    previousKey: string | null;
}

export const useHistory = (): HistoryProps => {
    
    const { key } = useLocation();
    
    const [history, setHistory] = useState<string[]>([]);
    const [currentKey, setCurrentKey] = useState<string | null>(null);
    const [previousKey, setPreviousKey] = useState<string | null>(null);

    const contemporaneousHistory = history.includes(key)
        ? history
        : [...history, key];
    const index = contemporaneousHistory.indexOf(key);
    const isHistoricRoute = index + 1 < contemporaneousHistory.length;
    const state = { index, isHistoricRoute, key, previousKey };

    if (history !== contemporaneousHistory) setHistory(contemporaneousHistory);

    if (key !== currentKey) {
        setPreviousKey(currentKey);
        setCurrentKey(key);
    }

    return state;
}

I now have just created a new routing library for react where this is possible.

https://github.com/fast-router/fast-router

Server-Side rendering is not supported. The rest should work fine. The library is mainly inspired by wouter -> https://github.com/molefrog/wouter

There are hooks for example usePathname which only cause a new render if the actual pathname changes (ignoring the hash and search) It is possible to select just a single property of the history.state and don't get a new render if any other values inside the state changes.

Related