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. Find me on Bluesky or Mastodon. I also have an RSS feed here