jx, a minimal reactive library for the frontend
It’s easy to fall in love with fast, seamless reactivity on the frontend, as popularized by Angular, React and Vue. The first time you use it – if you’ve ever used more procedural methods – is simply magical. The downsides (and there are many, which I won’t go into here) is that it often takes a lot of the code to make it all work. Alpine is “lightweight” and still clocks in at 15k gzipped. You can get a lot for that bandwidth, to be sure, but it can still feel heavy when you just need to sprinkle some light reactivity onto a couple of elements.
So, fueled by curiosity and probably quite a bit of hubris, I
present jx: yet
another
minimal,
incremental, reactive, DOM-based library for the frontend.
As taken from the documentation:
- minimal
- only what we really need, and barely even that
- no dependencies; modern, vanilla JS only
- as is: ~11k (with comments), minified: 2k, gzipped: <1k
- compare to Alpine: 43k minified and 15k gzipped
- compare to Vue: 100k minified and 38k gzipped
- incremental
- can be added to pages bit by bit without needing to take over
- pairs well with jQuery, and probably even Alpine
- should work just as well with a single element/attribute as with dozens
- reactive
- enables more declarative code and state
- elements are automatically updated to reflect changes in state
- DOM-based
- no virtual DOM, no template compilation, no reinventing the wheel (ha ha)
Here is a simple counter example:
<script src="https://where.ever/you/host/js.min.js"></script>
<div>Current value: <span data-jx-text="count"></span></div>
<div>
<button onclick="jx.count -= 1">Decrement</button>
<button onclick="jx.count += 1">Increment</button>
</div>
<script>
const jx = new JX({ count: 0 });
</script>
And to summarize what you see in the example:
-
no build step: just include the
.jsfile- no CDN either; just vendor it
- simple global state; because that’s probably enough for most pages & projects
-
DOM-based
-
reactivity updates are based on
data-jx-...attributes -
event handler attributes
trigger mutations
-
ignore the MDN warning, event handler
attributes are battle tested and easy to reason about;
they support
locality of behavior
and are basically the same as
@clicket al handlers that Vue and Alpine use heavily
-
ignore the MDN warning, event handler
attributes are battle tested and easy to reason about;
they support
locality of behavior
and are basically the same as
-
state mutations happen via the (global)
jx(or whatever you opt to call it) object
-
reactivity updates are based on
Available Attributes
From the documentation:
-
data-jx-init: runs code once, at initialization data-jx-text: updatestextContentdata-jx-html: updatesinnerHTMLdata-jx-value: updatesvalue-
data-jx-class: updates the CSS classes applied to the element -
data-jx-disabled: toggles thedisabledattribute -
data-jx-if: toggles thedisplay: nonestyle
Notes
-
data-jx-valueis only intended for<input>elements -
data-jx-initis the only one that doesn’t alter or update any elements, unless you do so explicitly in the code being executed -
all of these attributes expect to receive a string, except for
data-jx-ifand *data-jx-disabled, which expect a boolean (or boolean-ish value)
Computed values
In addition to passing simple scalar values into the
JS() constructor, you can also pass a second object of
functions that will be reevaluated/updated as their dependencies
update. An example that can increment 2 values and present their
sum:
<script src="https://where.ever/you/host/js.min.js"></script>
<div>
A is <span data-jx-text="countA"></span> B is
<span data-jx-text="countB"></span> and A+B is
<span data-jx-text="sum"></span>.
</div>
<div>
<button onclick="jx.countA += 1">Increment A</button>
<button onclick="jx.countB += 1">Increment B</button>
</div>
<script>
const jx = new JX(
{ countA: 0, countB: 0 },
{ sum: (jx) => jx.countA + jx.countB },
);
</script>
Why
I was working on a page (like, a real single page, not a routed-view-within-a-SPA sort of page) and I needed a simple, performant way to toggle some elements on and off, and to automatically update content … basic reactivity stuff. But, I couldn’t bring myself to bring in 15k+ of JS just to toggle an input. Before I knew it, I had become too busy scratching this itch to notice that I had fallen down a rabbit hole.
How
tl;dr when JX is instantiated, a
Proxy is set up to observe access to the state
properties as each data-jx-... attribute is initially
found and executed. This builds a set of dependencies between
attributes, computed values and data properties, which we cache and
then can execute later as state properties are updated. Finally,
another Proxy is returned that wraps the state data and
computed values.
This approach (observing and caching dependencies) was inspired by Hyperactiv, and allows JX to query the DOM only once (at instantiation), making updates very fast. There are some “costs” to this simplicity and speed, though; features that you don’t get from JX that you may be familiar with from Vue or Alpine. For example,
- no deep reactivity; only the top level of state is reactive
- no reactivity for new data keys; if it’s not included at construction, it’s not reactive
- elements added to the DOM after initialization are not reactive
I’m OK with this and haven’t found these to be an issue. If you have a use case where these are important, then you shouldn’t use JX.
When
I use this any time I need to sprinkle some JS into a page without
paying the (considerable) complexity tax of adding a
package.json, build step, bundle, etc. When I
do need complicated state management, or serious separation
of concerns, modules, etc, I reach for something else. But I have
found that I don’t need that very often.
Code
-
jx-1.0.0.js- the full, commented source code (10k) -
jx-1.0.0.min.js- minified byterser -m(2k)
There are tests for this, but I’m not uploading them yet. Neither do I intend to release this via tagged, version controlled releases. At least not yet. If you’re interested in using this, just vendor it and own it.
Conclusion
This was fun to explore and build; and I’ve found it fun, easy and performant to use. Please consider my itch scratched.