State
Something that is truly missing in web APIs is reactivity, the ability to react to changes instead of listening to events. A big promise is the tc39 proposal which is attempting to bring signals to JavaScript.
Until then, Markup exposes a standalone API that gives you that capability called state
.
The state API can be used with or without templates allowing you to create state stores, enhance Web Components, and even provide reactivity with raw DOM manipulation.
const [count, setCount] = state(0)
const btn = document.createElement('button')
btn.type = 'button'
btn.addEventListener('click', () => {
setCount((prev) => prev + 1)
})
effect(() => {
btn.textContent = `count: ${count()}`
})
document.body.append(btn)
The best part is when you combine state and templates to render DOM nodes that react to updates as they happen without having to manipulate the DOM yourself.
const handleClick = () => {
setCount((prev) => prev + 1)
}
html`<button type="button" onclick="${handleClick}">count: ${count}</button>`
Markup templates do not traverse the DOM to check for updates. Instead, it creates render effects in place to update the DOM exactly where and when needed.
Input
To initialize a state, you can optionally provide an initialValue
as well as a StateSubscriber
which is a function that will be called every time the state changes.
const [count] = state<number>(0, () => {
// react to change
})
When no input value provided, the default value is an empty string.
Return
The state
function will return and array with three functions, a StateGetter
, StateSetter
, and a StateUnSubscriber
.
const [count, updateCount, unsubscribe] = state(0, () => {
// will only get called once
})
updateCount(10)
unsubscribe()
StateGetter
The StateGetter
is a function you must call to get the current value of the state.
const [count] = state(0)
console.log(count()) // logs 0
StateSetter
The StateSetter
is a function you call with the new value for the state or a function that gets called with the current value and must return a new value.
const [count, updateCount] = state(0)
// provide a new value
updateCount(10)
// use current value and perform a calculation
updateCount(count() + 5)
// use the callback to update the value
updateCount((prev) => prev + 5)
Calling the StateSetter
with same value will not cause the subscribers to be called. A shallow comparison is made before updating the current value which prevents unnecessary updates.
const [count, updateCount] = state(0, () => {
// this will never get called
// given the setInterval update bellow
console.log(count())
})
setInterval(() => {
updateCount(0)
}, 1000)
StateUnSubscriber
In case you provide a StateSubscriber
as a second argument for the state
, you can then use the StateUnSubscriber
to stop listening to the state changes.
const [count, updateCount, unsubscribeFromCount] = state(0, () => {
// react to change
})
unsubscribeFromCount()
How it works?
The state is a synchronous value which means you can update it in one line and read it on the next.
setCount(10)
console.log(count()) // logs 10
Behind the scenes, state
is just a self managed subscription that works seamlessly with effect
and html
APIs.
Examples
Form input value and validation:
const [valid, setValid] = state(true)
const [value, setValue] = state('', () => {
if (value().length > 8) {
setValid(false)
}
setValid(true)
})