
Research
Two Malicious Rust Crates Impersonate Popular Logger to Steal Wallet Keys
Socket uncovers malicious Rust crates impersonating fast_log to steal Solana and Ethereum wallet keys from source code.
@crystallized/reactive-property
Advanced tools
Data parsing and reactive sync for an element attribute/property using Signals.
A tiny library for data parsing and reactive sync for an element attribute/property using Signals. Part of the Crystallized project.
npm i @crystallized/reactive-property @preact/signals-core
or
yarn add @crystallized/reactive-property @preact/signals-core
The ReactiveProperty
class takes advantage of fine-grained reactivity using Signals* to solve a state problem often encountered in building vanilla web components. Here's an example of what we're dealing with:
<my-counter count="1">
<output>1</output>
<button id="inc">Inc +</button>
<button id="dec">Dec +</button>
</my-counter>
It's a typical "counter" example: click the + or - buttons, see the counter update.
In component-speak, we see here that count
is a prop of the my-counter
component. In this particular instance it's set to 1
. Even with this very simple example, we run into an immediate conundrum:
count
attribute is not 1
. It's "1"
(a string). So the simplistic JavaScript code "1" + 1
doesn't result in 2
, it's 11
. You need to parse the attribute's string value to a number before using it as a property value.myCounterInstance.count
is available through the component API.myCounterInstance.count = 10
should result in count="10"
on the HTML attribute. Generally this means serializing values to strings, but in some cases it means removing the attribute. el.booleanProp = false
shouldn't result in a booleanprop="false"
attribute but should remove booleanprop
entirely.Given all this, you're left with two choices:
Unfortunately, the second option typically doesn't mean reaching for a library which only solves these problems, but one which attempts to address a host of other problems (templates, rendering lifecycles, and various other UI component model considerations).
Personally, I tend to like libraries which do one thing and one thing only—well. ReactiveProperty doesn't care about templates. Doesn't care about rendering lifecycles. Doesn't care about element base classes or mixins or controllers or hooks or any of that.
All it does it give you reactive properties. Boom. Done
And it does this thanks to the amazing new Signals library from the folks at Preact.
* Signals is not part of Preact. It's a very simple, small, zero-dependency library. It's usable within React as well as any other framework. That's why you can easily use it with any vanilla JS code! And because ReactiveProperty only uses a fraction of the Preact Signals API, you can even opt for a different signals library as long as the interface is the same (aka provides a value
getter/setter and a subscribe
method for a watch callback).
import { signal, effect } from "@preact/signals-core"
class MyCounter extends HTMLElement {
static observedAttributes = ["count"]
constructor() {
super()
// Set up a bucket to manage our reactive property definitions
this.attributeProps = {}
// Add a reactive property for `count`
this.attributeProps["count"] = new ReactiveProperty(
this, // pass a reference to this element instance
signal(0), // create a signal with an initial value
{
name: "count", // the name of the property
// attribute: "count-attr", // if you need a different attribute name
// reflect: false, // turn off the prop->attribute reflection if need be
}
)
}
connectedCallback() {
setTimeout(() => { // I always wait a beat so the DOM tree is fully connected
this.querySelector("#inc").addEventListener("click", () => this.count++)
this.querySelector("#dec").addEventListener("click", () => this.count--)
// Whenever the `count` value is mutated, update the DOM accordingly
this._disposeEffect = effect(() => {
const countValue = this.count // set up subscription
// We'll only re-render once the component has "resumed", since on first run the HTML is
// already server-rendered and present in the DOM
if (this.resumed) this.querySelector("output").textContent = countValue
})
// Allow any future changes to trigger a re-render
this.resumed = true
})
}
disconnectedCallback() {
// Dispose of the rendering effect since the element's been removed from the DOM
this._disposeEffect?.()
}
attributeChangedCallback(name, oldValue, newValue) {
this.attributeProps[name]?.refreshFromAttribute(newValue)
}
}
customElements.define("my-counter", MyCounter)
And a reminder of the HTML again:
<my-counter count="1">
<output>1</output>
<button id="inc">Inc +</button>
<button id="dec">Dec -</button>
</my-counter>
What I love about this example is you can read the code and immediately understand what is happening. What ends up happening may feel a bit magical, but there's really no magic at all.
Under the hood, there's a count
signal which we've initialized with 0
. this.count++
is effectively prop.signal.value = prop.signal.value + 1
and this.count--
is effectively prop.signal.value = prop.signal.value - 1
. The side effect function we've written will take that value and update the <output>
element accordingly whenever the count
attribute/property changes.
ReactiveProperty knows that the initial value of the property is a number type, so it always typecasts attribute changes from strings to numbers. Same for booleans (true/false
), arrays ([]
), and objects ({}
). Strings of course are easiest to deal with.
So whether the count
attribute is set/updated through HTML-based APIs, or the count
prop is set/updated through JS-based APIs, the signal value is always updated accordingly, and that then will trigger your side-effect.
And because you're using Signals, you can take advantage of computed values as well which unlocks a whole new arena of power:
import { signal, computed, effect } from "@preact/signals-core"
// add to the bottom of your `constructor`:
this.times100Signal = computed(() => this.count * 100 )
Now every time the count
prop mutates, the this.times100Signal.value
signal will equal that number times one hundred. And you can access this.times100Signal.value
directly in an effect to update UI with that value also!
You can set up multiple effects to handle different part of your component UI, which is why this approach is termed "fine-grained" reactivity. Instead of a giant render
method where your component has to supply a template handling all of the data your component supports, you can react to data updates in effects and surgically alter the DOM only when and where needed. And the potential is high for an additional, slightly-more-abstract library to take advantage of this to provide markup-based, declarative bindings between DOM and reactive data.
Hmm… 🤔
Crystallized uses the Modern Web Test Runner and helpers from Open WC for its test suite.
Run npm run test
to run the test suite, or npm run test:dev
to watch tests and re-run on every change.
git checkout -b my-new-feature
)git commit -am 'Add some feature'
)git push origin my-new-feature
)MIT
FAQs
Data parsing and reactive sync for an element attribute/property using Signals.
The npm package @crystallized/reactive-property receives a total of 1 weekly downloads. As such, @crystallized/reactive-property popularity was classified as not popular.
We found that @crystallized/reactive-property demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Socket uncovers malicious Rust crates impersonating fast_log to steal Solana and Ethereum wallet keys from source code.
Research
A malicious package uses a QR code as steganography in an innovative technique.
Research
/Security News
Socket identified 80 fake candidates targeting engineering roles, including suspected North Korean operators, exposing the new reality of hiring as a security function.