reactjsreact-hooksreact-router-dombrowser-historyhtml5-history

Creating custom history back and forwards buttons using a React hook


I would like to create custom history back and forwards buttons using a React hook.

For reference, in case it helps, I'm trying to replicate the behaviour that can be seen in Spotify's web app. Their custom forwards and back buttons integrate seamlessly with the browser history buttons.

I think I have it mostly working, but with one issue. Here is my React hook:

import { useState, useEffect } from 'react';
import { useHistory } from 'react-router-dom';

const useNavigationHistory = () => {
  const history = useHistory();

  const [length, setLength] = useState(0);
  const [direction, setDirection] = useState(null);
  const [historyStack, setHistoryStack] = useState([]);
  const [futureStack, setFutureStack] = useState([]);

  const canGoBack = historyStack.length > 0;
  const canGoForward = futureStack.length > 0;

  const goBack = () => {
    if (canGoBack) {
      history.goBack();
    }
  };

  const goForward = () => {
    if (canGoForward) {
      history.goForward();
    }
  };

  useEffect(() => {
    return history.listen((location, action) => {
      // if action is PUSH we are going forwards
      if (action === 'PUSH') {
        setDirection('forwards');
        setLength(length + 1);
        // add the new location to the historyStack
        setHistoryStack([...historyStack, location.pathname]);
        // clear the futureStack because it is not possible to go forward from here
        setFutureStack([]);
      }
      // if action is POP we could be going forwards or backwards
      else if (action === 'POP') {
        // determine if we are going forwards or backwards
        if (futureStack.length > 0 && futureStack[futureStack.length - 1] === location.pathname) {
          setDirection('forwards');
          // if we are going forwards, pop the futureStack and push it onto the historyStack
          setHistoryStack([...historyStack, futureStack.pop()]);
          setFutureStack(futureStack);
        } else {
          setDirection('backwards');
          // if we are going backwards, pop the historyStack and push it onto the futureStack
          setFutureStack([...futureStack, historyStack.pop()]);
          setHistoryStack(historyStack);
        }
        setLength(historyStack.length);
      }
    });
  }, [history, length, historyStack, futureStack]);

  return { canGoBack, canGoForward, goBack, goForward };
};

export default useNavigationHistory;

In my testing this all seems to work fine when navigating forwards and back between various different pages.

The Problem

If I navigate forwards by alternating between the same 2 pages, for example:

/home
/about
/home
/about
/home
/about

...then my logic to determine if we are going forwards or backwards falls apart.

I think it's this line:

if (futureStack.length > 0 && futureStack[futureStack.length - 1] === location.pathname) {

because the forwards pathname and the backwards pathname are identical, so it thinks I'm going forwards even when I'm going backwards.

I've been trying to figure out how I could resolve this, but haven't managed to get something working.

Is anyone able to help?

Maybe my solution is flawed and I need an entirely different method, I'm not sure.


Solution

  • It turns out that the data returned by react-router-dom's useHistory hook already includes a unique key value for every item in the history.

    So the solution was simply to swap every instance of location.pathname for location.key in the hook, and now it behaves as desired.