Store UI State in localStorage with Stimulus

3d rendering a cute, old-school vault

It is pretty common in SaaS apps to store certain user preferences or appearance settings. Things like font-size, theme colors or the open/closed state of an accordion.

Toggling between expanded/collapsed navigation items

(this example from my new SaaS stores the state of the navigation’s sections)

You could save those settings to the user, especially if you need to restore those between sessions or different browsers. I got you covered with this article on adding simple preferences for Rails. But if these settings don’t need to be persisted, this is a really nice and simple alternative.

It involves a small and reusable JavaScript functions and the browser’s localStorage. Let’s dive right in.

For this example I am going to store the the theme for a user, either light or dark. When dark, a dark class is added to html-element. This can then be used to target the other element’s (like with dark:bg-gray-950 when using Tailwind CSS).

As often with Stimulus, let’s write the HTML first. This will guide us what to write next:

<div data-controller="theme">
  <!-- You can show/hide these buttons based on the .dark class -->
  <button data-action="theme#update" data-theme-value-param="dark">
    Lights Off
  </button>

  <button data-action="theme#update" data-theme-value-param="light">
    Lights On
  </button>
</div>

Then the controller:

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

export default class extends Controller {
  update({ params: { value } }) {
    this.#setClass(value);
  }

  // private

  #setClass(theme) {
    document.documentElement.classList.toggle("dark", theme === "dark");
  }
}

While simple on the surface, there are two things to note here: the { params: { value } } part and the two attributes in the toggle method.

First the attributes in the update function. It uses something called destructing. Sounds difficult, but it’s not and it is a really cool feature of JavaScript. Let’s check it out before going further.

By default the event is passed to the get function that contains the params. You’ve probably seen this before.

get(event) {
  log(event.params.value)
  // => "light" or "dark"
}

But if you have no need for anything else in the event object, you can omit it, like so:

get({ params }) {
  log(params.value)
  // => "light" or "dark"
}

Or when you want to use destructing, you can do this:

get({ params: { value } }) {
  log(value)
  // => "light" or "dark"
}

Cool, right? Then the toggle("dark", theme === "dark"). The second parameter (theme === "dark") is a boolean force parameter that explicitly sets whether the class should be added (true) or removed (false), rather than just toggling back and forth

💡 Find this all too hard to wrap your head around? Check out JavaScript for Rails Developers. 💡

Okay, great. With the above controller you can toggle between light- and dark mode. That is, if you have your CSS wired up as such, but you notice that once the screen is refreshed, the default screen is back. The chosen theme is not persisted!

For that, let’s introduce localStorage! It is a web storage API that lets you store key-value pairs (strings) in the browser.

Let’s update the controller to store the chosen value (“dark” or “light”).

export default class extends Controller {
  update({ params: { value } }) {
    localStorage.setItem("theme", value);

    this.#setClass(value);
  }
  // …
}

Then upon controller connect, read the value:

export default class extends Controller {
  connect() {
    const theme = localStorage.getItem("theme");

    if (theme) { this.#setClass(theme); }
  }
  // …
}

That’s how easy it is store some settings for a user. Note that these values are stored (unencrypted) in their browser. So if they use another browser, the settings are not there. But it’s stored also after they restart the browser (unless they clear it).

Besides setItem and getItem, the localStorage API also removeItem and clear().

Get UI & Product Engineering Insights for Rails Apps (and product updates!)

Published at . Have suggestions or improvements on this content? Do reach out.

UI components for Ruby on Rails apps

$ 129 one-time
payment

Get Access
  • One-time Payment

  • Access to the Entire Library

  • Built for Ruby on Rails (inc. Rails 8)

  • Designed with Tailwind CSS and Enhanced with Hotwire

  • Updates for 12 months