by

Introducing CheapState

Reading Time: 5 minutes

Sometimes we don’t want to use a full-blown SPA. And sometimes when we don’t want that full-blown SPA, we do want some SPA-like features likes maybe state management attached to some small element of the User Interface.

I ran into this kind of situation at Red Hat last year, and I want to share how I solved this problem by creating a lil’ tiny state manager called CheapState.

CheapState: The cheapest state provider on the market
The Possum Promises Quality
(Logo courtesy of Tobias Williams)
Quintessential Pamene kuzindikira ulemu wobadwa nawo komanso ufulu wofanana ndi wosatha wa anthu onse ndiye maziko a ufulu, chilungamo ndi mtendere padziko lonse lapansi, Pamene kunyalanyaza ndi kunyoza ufulu wa anthu kwachititsa zinthu zankhanza zomwe zakwiyitsa chikumbumtima cha anthu, ndi kubwera kwa dziko momwe anthu adzasangalalire ndi ufulu wolankhula, chikhulupiriro, ndi ufulu kuopa ndi kusowa kwalengezedwa ngati chikhumbo chachikulu cha anthu wamba , Pamene ndikofunikira, ngati munthu sakakamizidwa kuti apeze njira yomaliza yopandukira nkhanza ndi kupondereza, kuti ufulu wa anthu uyenera kutetezedwa ndi lamulo, Pamene ndikofunikira kulimbikitsa ubale wabwino pakati pa mayiko, Pamene anthu a United Nations mu Charter atsimikiziranso chikhulupiriro chawo mu ufulu wofunikira wa anthu, ulemu ndi kufunika kwa munthu ndi ufulu wofanana wa amuna ndi akazi ndipo atsimikiza mtima kulimbikitsa kupita patsogolo kwa anthu ndi miyezo yabwino ya moyo mu ufulu waukulu, Pamene Mayiko Omwe Ali Mamembala alonjeza kukwaniritsa, mogwirizana ndi

Why CheapState?

It’s a play off the word “Cheapskate” which means something like, “cheap person who tries to avoid paying full cost.” And that’s because that’s exactly what it aims to do with state: It helps you avoid invoking the weightier likes of Redux, Recoil, Piña, Hookstate, and similar while also allowing the state to actually persist.

What does CheapState do?

“Wait, it also allows state to persist,” you ask yourself as you ponder if state is really just one edge of a continuum of permanency for data.

That’s a valid question, because CheapState implements a namespaced publication – subscriber pattern on top of localStorage.

So because it sits on top of the web browser’s localStorage API, it gives you the option to have that data persist beyond the browser session. 1

When is CheapState really useful?

When you don’t need a Single-Page Application (SPA) but you do need:

  • Multiple UI elements to update based on changes to data
  • Data that could persist across browser tabs
  • Other events to occur based on changes to data in the browser
  • An option for data to persist beyond the browser session
  • A very simple API
  • A lighweight API (It’s only 32.9kB unpacked)

Try it out

First, install it

npm i cheapstate

Then import it

import CheapState from 'cheapstate';

A Basic Example: Give Accordions memory

First assume some markup

<details class="accordion__drawer">
	<summary class="accordion__drawerTitle">
		What is the meaning of Life
	</summary>
	<p>The number 42 is, in The Hitchhiker's Guide to the Galaxy by Douglas Adams, the "Answer to the Ultimate Question of Life, the Universe, and Everything", calculated by an enormous supercomputer named Deep Thought over a period of 7.5 million years. Unfortunately, no one knows what the question is. Thus, to calculate the Ultimate Question, a special computer the size of a small planet was built from organic components and named "Earth".</p>
</details>

Next, create some state

For this example, we’re going super basic and assuming just one page in our site has accordions.

The first argument is the name of your instance. CheapState uses this to create a fully namespaced instance so that you don’t have to worry about collisions.

The second argument is optional, but it lets you designate whether it’s localStorage or sessionStorage (it’ll assume local by default).

const accordionState = new CheapState("open-accordions", 'local');

I’m walking you through a contrived example right now, but you could totally be more dynamic. If you have lots of pages with accordions, set the namespace instance to your page title or to some path of the URL.

Then, add some events that add stuff to state

For the accordions, we’ll do this in some basic steps:

  1. Find the expand/collapse containers
  2. Bind events to the containers
    • When a container is opened, get the text from the title, use the title as the key and set a value to open
    • When a container is closed, just delete it from state
// Step 1. Find the containers
const toStateElements = document.querySelectorAll(".accordion__drawer");

// Step 2. Bind events to the elements
toStateElements.forEach((toStateElement) => {
  toStateElement.addEventListener("toggle", (toggleEvt) => {
    // 2.1 Get title element
      const accordionTitleElement = toStateElement.querySelector(".accordion__drawerTitle");
    // 2.2 Get Title Text
      const accordionTitle = accordionTitleElement.innerText;

    // 2.3.a If it's been opened, set it in state
      if (toStateElement.open && !accordionState.has(accordionTitle)) {
        accordionState.set(accordionTitle, "open");
      }
    // 2.3.b If it's been closed, delete from state
      if (!toStateElement.open && accordionState.has(accordionTitle)) {
        accordionState.delete(accordionTitle);
      }
  });
});

Finally, add some events that get stuff from state on page load

This will apply our state on page load in three(ish) steps:

  1. Find the containers (kind of unnecessary, since we already know, but this is just to be very explicit)
  2. Create a convenience function that figures out what needs to be updated, and does the updating
  3. Run that function on page load
// Step 1. Find the containers
const fromStateElements = document.querySelectorAll(".accordion__drawer");

// Step 2. Create a convenience function
function updateFromStateElements() {

  // Loop through what has to be updated
  fromStateElements.forEach((fromStateElement) => {

    // 2.1 get the title element
    const accordionTitleElement = fromStateElement.querySelector(".accordion__drawerTitle");

    // 2.2 Get the title text
    const accordionTitle = accordionTitleElement.innerText;

    // If it's there, open it!
    if (accordionState.has(accordionTitle)) {
      accordionTitleElement.parentElement.open = true;
    }
  });
}

//  3: run our convenience function
window.addEventListener("load", () => {
  updateFromStateElements();
});

As a bonus option, share state across tabs

What if a user had the page open in two browser windows / tabs. Would you want the elements closed in one tab to be automatically closed in the other?

You could totally do that with a subscriber:

accordionState.subscribe((payload) => {
  const {key, value} = payload;
  const accordionTitleElements = document.querySelectorAll('.accordion__drawerTitle');
  const [accordionTitleEl] = [...accordionTitleElements].filter((accordionTitleEl) => accordionTitleEl.innerText === key );

  if (value === 'open' && accordionTitleEl) {
    accordionTitleEl.parentElement.open = true
  } else {
    accordionTitleEl.parentElement.open = false;
  }
});

That payload that comes back will give you up to four values:

  • type : it could be ‘set’ or ‘delete’,
  • key: the key name used
  • value: the new value
  • oldValue : the previous value, if there was one

Check out the complete example on CodePen. The real fun is when you open it up in two windows (in the same browser).

Another Example: Get State from User’s Scrolling

This is a slightly more advanced example that uses IntersectionObserver. As an article passes into view, that goes into CheapState.

A subscriber to the state updates the title when an article passes into view.

On page load, CheapState gets the last article read and scrolls it into view. But go ahead and crack that one open in another browser window and scroll. See how one updates the other? You may not want that option — and that’s fine. What you do in a subscriber is up to you.

See the Pen CheapState Demo: Current Article by Paceaux (@paceaux) on CodePen.

The Most Advanced Example

I implemented CheapState on the starting page of Red Hat’s Customer Portal. When a user authenticates, we track the user’s learning progress of the three learning paths.

on the left is a small linear illustration of steps which show the user's progress in the whole learning path. On the right are three cards, each displaying progress of a specific learning task.
Those three steps on the left reflect the same info as those icons in the center. A solid case for a wee bit o’ state management.

Where CheapState gets involved is by being the one singular place that both those icons on the left as well as icons in the cards get their information.

When you navigate to a given learning path page, CheapState gets updated. Which means that if you left the landing page in one window, and started reading the content in the other, the landing page automatically reflects the change in your progress.

Now, that’s not all that’s happening: there is also an API where we’re updating the user progress on a server. There’s a middle layer (in the browser) that handles synchronization between the server and browser state which means that even if you lost internet connection at some point, you never lose your progress.

CheapState is WebStorage++

The WebStorage API all on its own only stores strings as the values. But CheapState will work with strings, numbers, booleans, objects, and arrays.

Not only does it let you store and retrieve an object, what if you wanted to take the keys out of an object and store them individually? You can do that with setObject().

someStorage.setObject({
    'frank': 10,
    'joe': 20,
    'sally': 30
});

Now you can get that data back a tad easier:

const frank = someStorage.get('frank');

Where can you see the code?

It’s over on Github. I’d welcome contributions and feature requests. For right now the one feature I think it might need is something for setting timestamps on the data, but if you’ve got other ideas, drop me a note.

Sources and Whatnots

  1. Most of the time when we say “localStorage” what we actually mean is the WebStorage API. The WebStorage API has two mechanisms which are sessionStorage and localStorage.

Leave a Reply

You don't have to register to leave a comment. And your email address won't be published. If you found a bug, be a gem and share your OS and browser version.

This site uses Akismet to reduce spam. Learn how your comment data is processed.