Global state and stores with web components
Published 2025-01-18, About 8 minute read.
If you build an application that needs to scale at all, you probably started to think about state. You might start to feel the pain of trying to orchestrate state between components and making them react to each other appropriately without creating a big bowl of spaghetti. This concern about application architecture pops up naturally and quickly. Most people think "if only this piece of data and it's logic could stand apart from components, and the components would just know when the data changes."
Enter reactive stores.
So what if you're in the web component world? Or if you simply want to stay as vanilla/platform-oriented as possible? Let's say you aren't in a framework that supplies services or stores, and you want to think through what's possible with some bare-bones Javascript?
We're going to start with the simplest of the simple as far as Javascript goes. From there, we're going to build up and use more recent APIs and paradigms, and let's see where we end up!
Simplest of the Simple: A shared reference
And this is not just any shared reference! This reference is going to be shared through the Window!
Mind blowing, I know! Now, as a front end dev one would cringe at this idea, since this "pollutes the window" and the data provided here is also not private. These are good points, but a whole different discussion...because as I've lived in web component land I've come to question some long-time-held beliefs- and I'm not the only one.
Let's get back to focusing on the store. In this state it holds a reference to an object. Each component gets a reference to the object, and if that object changes each would see the data change inside the object. BUT, the way things are right now, the component isn't aware if the data changes. Take a look at the console logs in the next example.
Okay, so the global data actually does change, but the components just sit there! So we need a way for the components to react.
Yes, the component will react now! But You will notice pretty quick, there's a lot wrong with the above example.
First off: in order for our components to react, we need to call a method to
make the component "update" the DOM with the new state. This method is
very imperative!. We need to go through an update text in the DOM and
update the value of the text input directly. Why don't we just call
this.render()
again? Well, the way we set up render right now
would wipe out the DOM as is and rerender everything... we would lose our
event listeners that handle the input and we would lose keyboard focus. Give
it a try, modify the example to just call render instead of update. It's an
unpleasant experience! Imperative updates are not ipso facto bad, but we are
definitely accustomed to just being able to re-render and having the
framework resolve the details.
Rendering details aside, you'll notice it looks like each component has it's own private state. This is an illusion, though, because we really are changing the global object. It's just that other components that aren't making the change have no idea to update as well. So in our case here, we have state ping-ponging between the two components, and they are each keeping a private state in their own value for their own input. They are effectively overwriting the state with their own private state on every input.
The store should handle reactivity signalling, not our components
In the spirit of trying to do the simplest possible change to incrementally improve here, I'm going to keep the update method in the component. I'm going to move the responsibility of who signals changes to the store.
This means however, that consumers need to subscribe to changes when they happen- or at least opt in to be notified. So I'm going to create a pub/sub system so the data object can notify any component that subscribes to changes.
The pub sub system doesn't event tell the consumer what has changed- it just lets them know there is a change. We let the consumer tell the store what function to run when the store changes.
But can we do event better?
YES. Let's make things nice for the consumer. What if we made it that
consumers don't have to notify when something changes? A consumer like these
components may forget to call this.notify()
whenever there are
state changes. So we have some Javascript magic we can employ.
Using getters and setters to improve the developer experience
We're going to do something similar to signals. You may already be familiar with these bad boys. Vue used something nearly identical. Solid made them famous. Preact was an early adopter using them. Angular just recently adopted them. Lit is investigating using them (now it's available as an experimental feature), and there's even going to be a vanilla javascript API for them hopefully soon™️.
But for the uninitiated, here's what using a signal looks like:
You create data and register functions to run when that data gets changed.
So maybe you can see where I'm going with this? We'd like a store to provide
these two signal
and effect
functions so that we can
create data in a store, and components could register their update functions
with effect()
.
The bonus for doing this extra work? We don't have to call a notification function every time state changes and we need consumers to react.
Below is a very vanilla and very rough toy example of a signal-like reactivity.
Alright, let's use the real stuff
Enough with the hand rolled toys! Let's pull in two libraries to make this vanilla web component and homemade store work like a modern developer would expect.
We're going to use lighterhtml to handle our rendering. And we're also going to pull in preact signals to handle our reactivity
The end result is what we had above with a render function that doesn't
require the imperative updates that we are doing before, and we will use
effect
to handle re-rendering when state changes.
It's interesting to me how close we can get to a web component framework with web components, a reactivity system, and a DOM renderer.
The real stuff: redux
At this point we can introduce a store library that is vanilla js (framework agnostic) and serves us just as well as the signals do to this point. nanostores is a nice, tiny library that creates a reactive store for you.
Also, we're going to go back to using a lit element. We can set it up so that
when the store changes state values, we can run
this.requestUpdate()
which is the lit way to ask a component to
re-render.
As you can see, we can get these components nearly as concise and clean as components you would see in frameworks- without any compiling magic!
That's only just the beginning...
One thing I expected to go into more was the mode of sharing stores. As I continued writing this post, I realized the truth is there are lots of ways to pass the reference of a store to different components. In this discussion, we just passed it along by making the store available on the window. Maybe this is something that will return to being in style?
Another solid way to pass a reference down the DOM is to use events-based providers. If you don't make your own, Lit context is fantastic and is based on the web components community group context protocol.
And yet another viable way to pass stores is to simply import/export them in modules if your web components are all part of the same monorepo. As long as components are part of the same build process with the same node modules, if two components import the same store from the same module they'll be importing the same instance. This makes stores sharing super easy.
All that to say, the real linchpin for reactive stores is the reactivity and re-rendering component. Passing the reference to your shared state shouldn't be difficult.
That's all for now
I hope building a reactive store with components using vanilla tools was interesting. Add a comment below or find me on Bluesky or Mastadon. I also have an RSS feed here