0

In a React & Next.js app I'm trying to implement a back button. To do that I've added currentPath and prevPath to the session storage in the _app.js file.

// pages/_app.js
function MyApp({ Component, pageProps }) {
  const router = useRouter();

  useEffect(() => {
    const storage = globalThis?.sessionStorage;
    if (!storage) return;
    
    storage.setItem('prevPath', storage.getItem('currentPath'));
    storage.setItem('currentPath', globalThis.location.pathname);
  }, [router.asPath]);

  return <Component {...pageProps} />
}

export default MyApp

Then I am trying to get this data in a Navigation.js component.

// Navigation.js
const router = useRouter();
const [prevPath, setPrevPath] = useState('/');

useEffect(() => {
  const getPrevPath = globalThis?.sessionStorage.getItem('prevPath');
  setPrevPath(getPrevPath);
}, [router.asPath]);

return (
  // …
  <Link href={prevPath || '/'}>
    <a>Back</a>
  </Link>
  //…
)

While the session storage works correctly, the value returned is one from the previous page (that is previous page's prevPath) instead of the current one. Technically, asking for a currentPath instead of a prevPath would be the solution to what I'm trying to do but I'd like to (learn to) do it the right way.

Additional info:
I've tried to get data with async/await but it didn't make any difference.

useEffect(async () => {
  const getPrevPath = await globalThis?.sessionStorage.getItem('prevPath');
  setPrevPath(getPrevPath);
}, [router.asPath]);

Also, earlier in a day (the implementation was different) I've tried as an experiment adding a delay of 1/1000th of a second and it did make it work correctly. Given that, I'm not confident waiting a fixed number of seconds (or a fixed fraction of a second) would be a good solution (could someone confirm?).

Would appreciate the help.

ytrewq
  • 59
  • 1
  • 10
  • I'm a bit confused about why you're tracking routes through local storage. If you're sole intention is to just go back to the previous page, then you use a button with [router.back](https://nextjs.org/docs/api-reference/next/router#routerback) – Matt Carlotta Jul 25 '21 at 19:13
  • @MattCarlotta I'd like it to function like every other link on the website, for example to have a url displayed on hover – ytrewq Jul 25 '21 at 22:06

1 Answers1

2

Problem

I'm assuming you want to add and remove history (similar to a real browser history) instead of just constantly replacing the history with whatever route was previous. Instead of constantly replacing the pathname upon a route change, you'll want to conditionally add/remove it from some sort of history.

Solution

Here's a hook that utilizes an Array (basically a flat array of asPath strings -- you may want to limit the size of the Array to prevent performance issues):

import * as React from "react";
import { useRouter } from "next/router";

const usePreviousRoute = () => {
  const { asPath } = useRouter();
  // initialize history with current URL path
  const [history, setHistory] = React.useState([asPath]);
  const lastHistoryIndex = history.length - 2;
  // get second to last route in history array
  const previousRoute = history[lastHistoryIndex > 0 ? lastHistoryIndex : 0];

  const removeHistory = () => {
    // get current history
    setHistory((prevHistory) =>
      // check if the history has more than 1 item
      prevHistory.length > 1
          // if it does, remove the last history item
        ? prevHistory.filter((_, index) => index !== prevHistory.length - 1)
          // else don't remove any history
        : prevHistory
    );
  };

  React.useEffect(() => {
     // get current history
    setHistory((prevHistory) =>
       // check if the last history item is the current path
      prevHistory[prevHistory.length - 1] !== asPath
        // if not, add current path to history
        ? [...prevHistory, asPath]
        // else don't add any history
        : prevHistory
    );
  }, [asPath]);

  return { previousRoute, removeHistory };
};

export default usePreviousRoute;

With capped history:

  React.useEffect(() => {
    // get current history
    setHistory((prevHistory) =>
      // check if last history item is current path
      prevHistory[prevHistory.length - 1] !== asPath
      // if not...
        ? [
            // check if history has more than 10 items
            // spread result into shallow copied array
            ...(prevHistory.length > 9
              // if it does have more than 10 items, remove first item
              ? prevHistory.filter((_, index) => index !== 0)
              // else don't remove history
              : prevHistory),
            asPath
          ]
        // else don't remove history
        : prevHistory
    );
  }, [asPath]);

Demo

Source Code:

Edit NextJS - Back Link (modular - array)

Browser Demo URL: https://knfoj.sse.codesandbox.io/

Demo Code

Navigation.js

/* eslint-disable jsx-a11y/anchor-is-valid */
import * as React from "react";
import Link from "next/link";
import { useHistoryContext } from "../../hooks/useRouteHistory";
import GoBackLink from "../GoBackLink";
import styles from "./Navigation.module.css";

const Navigation = () => {
  const { history } = useHistoryContext();

  return (
    <>
      <nav className={styles.navbar}>
        {[
          { title: "Home", url: "/" },
          { title: "About", url: "/about" },
          { title: "Example", url: "/example" },
          { title: "NoLayout", url: "/nolayout" }
        ].map(({ title, url }) => (
          <Link key={title} href={url} passHref>
            <a className={styles.link}>{title}</a>
          </Link>
        ))}
      </nav>
      <GoBackLink />
      <div className={styles.history}>
        <h4 style={{ marginBottom: 0 }}>History</h4>
        <pre className={styles.code}>
          <code>{JSON.stringify(history, null, 2)}</code>
        </pre>
      </div>
    </>
  );
};

export default Navigation;

useRouteHistory.js

import * as React from "react";
import { useRouter } from "next/router";

export const HistoryContext = React.createContext();
export const useHistoryContext = () => React.useContext(HistoryContext);

export const usePreviousRoute = () => {
  const { asPath } = useRouter();
  const [history, setHistory] = React.useState([asPath]);
  const lastHistoryIndex = history.length - 2;
  const previousRoute = history[lastHistoryIndex > 0 ? lastHistoryIndex : 0];

  const removeHistory = () => {
    setHistory((prevHistory) =>
      prevHistory.length > 1
        ? prevHistory.filter((_, index) => index !== prevHistory.length - 1)
        : prevHistory
    );
  };

  React.useEffect(() => {
    setHistory((prevHistory) =>
      prevHistory[prevHistory.length - 1] !== asPath
        ? [...prevHistory, asPath]
        : prevHistory
    );
  }, [asPath]);

  return { history, previousRoute, removeHistory };
};

export const HistoryProvider = ({ children }) => {
  const historyProps = usePreviousRoute();

  return (
    <HistoryContext.Provider
      value={{
        ...historyProps
      }}
    >
      {children}
    </HistoryContext.Provider>
  );
};

export default HistoryProvider;

_app.js

import * as React from "react";
import HistoryContext from "../hooks/useRouteHistory";

const App = ({ Component, pageProps }) => (
  <HistoryContext>
    <Component {...pageProps} />
  </HistoryContext>
);

export default App;

index.js

import Layout from "../components/Layout";

const IndexPage = () => (
  <Layout>
    <h1>Index Page</h1>
    <p>
     ...
    </p>
  </Layout>
);

export default IndexPage;
Matt Carlotta
  • 18,972
  • 4
  • 39
  • 51
  • Thank you, I like this approach and I appreciate the demo. I know it's meant just as an example but the way my app is currently structured is that Navigation.js is imported in a Layout.js component, which in turn is imported in what would be equivalent to index.js, about.js and example.js, and I can't quite figure out how to integrate this solution into my app without completely changing the app's structure. Do you think it would be possible? – ytrewq Jul 27 '21 at 12:53
  • I’d need to see a codebase (like a GitHub repo) before I could offer any project-specific advice. That said, you could make this more modular (so it's not imported into `_app.js`), but then you'll need to use some sort of persistent layer. Something like LocalStorage would work, but can get tricky for SSR routes. If tracking history to a specific user is important, then a database might make more sense. – Matt Carlotta Jul 27 '21 at 14:59
  • Here's a [modular approach demo](https://codesandbox.io/s/nextjs-back-link-modular-soffd). This allows you to use history in or outside of a layout. – Matt Carlotta Jul 27 '21 at 16:51
  • Brilliant! I was able to integrate it successfully, thank you so much! I'm sorry to ask one more question. As you've mentioned before, it does return a [slug] for dynamic routes. How should it be adjusted to support them? – ytrewq Jul 27 '21 at 20:57
  • I believe you can use `asPath` like you were using above, but it may not work for static routes. As such, you may have to use the [window.location.pathname](https://www.w3schools.com/js/js_window_location.asp). – Matt Carlotta Jul 27 '21 at 21:07
  • `asPath` worked for me. Thank you for helping! – ytrewq Jul 27 '21 at 22:16
  • Managed to simplify the history hook using an Array instead of a Map. See updated answer with demo and code. I like this approach better as it avoids mutating the original state and doesn't involve tracking an index. – Matt Carlotta Jul 28 '21 at 00:43
  • In addition, capping its size is very straight forward and takes no more than a line of code. Thank you! – ytrewq Jul 28 '21 at 15:47
  • Last update (I promise): I added a hard cap to the demo (and provided code notes). Now, it's hard capped to 10 items. Adding more history, after 10 items, removes the first history item while simultaneously adding the current path as the last history item. This way, you can still track new history while preventing an unnecessary large array. – Matt Carlotta Jul 28 '21 at 17:17
  • Awesome, thank you again! Before I've used an `if(history.length > 9) history.shift();` placed above the return statement but your way seems to be better! – ytrewq Jul 29 '21 at 20:50