How to save a recently viewed list of pages with Stimulus and localStorage

How to save a recently viewed list of pages with Stimulus and localStorage

Creating a "recently viewed pages" list can significantly enhance the user experience by providing easy navigation and a history of recently accessed content. Seeing how we are already using Rails, we are going to leverage Stimulus. Also, instead of storing the information on the server, we will use localStorage to persist the viewed pages data across sessions.

Why Use Stimulus and localStorage?

Usually for UI interactions, I prefer keeping things as lightweight as possible. This is why I prefer AlpineJS over Stimulus.

In this case, however, Stimulus is a good choice because there's a bit more logic in the JavaScript.

We will also use localStorage as a client-side storage mechanism. It’s ideal for saving non-sensitive information like page history because the data is stored even after the user refreshes or closes the browser.

The downside to using localStorage is that if the user logins from another computer, this is not synchronized. It is possible to sync this information with additional logic in the Stimulus controller, but if your use case is like mine, this is probably not necessary.

The Goal

Our goal is to build a recently viewed pages list that:

  • Tracks the pages visited by the user.

  • Stores metadata such as the page name and a short description.

  • Displays up to the last 20 recently visited pages.

  • Persists this data across sessions using localStorage.

The Stimulus Controller

Below is a Stimulus controller that handles tracking and storing the user’s visited pages:

// app/javascript/controllers/recently_viewed_controller.js
import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static targets = ["currentPath"];

  connect() {
    if (this.currentPathTargets.length !== 0) {
      let name = this.currentPathTarget.dataset.name;
      this.saveCurrentPath(name);
    }
  }

  saveCurrentPath(name) {
    let path = window.location.pathname;

    // Create an object with path, name, and description
    let pathObject = {
      path: path,
      name: name
    };

    // Remove existing objects with the same path
    let visitedPaths = this.removePathsContaining(path);

    // Add the new object to the beginning of the array
    visitedPaths.unshift(pathObject);

    // Keep only the last 20 entries
    if (visitedPaths.length > 20) {
      visitedPaths = visitedPaths.slice(0, 20);
    }

    // Save the updated array back to localStorage
    localStorage.setItem("visitedPath", JSON.stringify(visitedPaths));
  }

  removePathsContaining(substring) {
    // Get the existing array from localStorage
    let visitedPaths = JSON.parse(localStorage.getItem("visitedPath")) || [];

    // Filter out objects where the path contains the substring
    visitedPaths = visitedPaths.filter(
      (item) => !item.path.includes(substring)
    );

    return visitedPaths;
  }

  resetPaths() {
    // Clear the visitedPath item from localStorage
    localStorage.removeItem("visitedPath");
  }
}

To activate this controller and to track specific pages, you will need to add this to the body tag:

<body data-controller="smooth-scroll recently-viewed">
  <div data-recently-viewed-target="currentPath" data-page-name="<%= @page_name %>"></div>
  /* Your code */
</body>

You have the option of refactoring the above code or combining them. In my use case, I wanted to add the controller to the body tag to activate it, and then optionally have it save the page if I added the inner div tag.

Using the list from localStorage

Now that you have the list, you can access it with something like this:

window.recentlyVisitedPaths = JSON.parse(localStorage.getItem("visitedPath")) || [];

The values will now be returned as JSON and you can render it using Stimulus or something similar.

Here is a demo of how I use it in my app at Proggy.io: