hooktml
Version:
A reactive HTML component library with hooks-based lifecycle management
84 lines (74 loc) • 2.4 kB
JavaScript
import { tryCatch } from '../utils/try-catch.js'
import { isFunction, isUndefined } from '../utils/type-guards.js'
import { logger } from '../utils/logger.js'
/**
* @template T
* @typedef {Object} Signal
* @property {T} value
* @property {Function} destroy
* @property {Function} toString
* @property {Function} subscribe
*/
/**
* A lightweight reactive primitive for storing local state.
*
* @template T
* @param {T} initialValue - The initial value to be stored in the signal
* @returns {Signal<T>} A signal object with a value property
*/
export const signal = (initialValue) => {
// Store value in a container object to avoid direct reassignment
const state = { current: initialValue }
// Store subscribers in a Set for uniqueness and O(1) lookup
const subscribers = new Set()
const signalObject = {
get value() {
// Track this signal as a dependency if we're in a tracking context
if (!isUndefined(globalThis) && globalThis.__HOOKTML_TRACK_SIGNAL__) {
globalThis.__HOOKTML_TRACK_SIGNAL__(signalObject)
}
return state.current
},
set value(newValue) {
// Optional: Check if the value hasn't changed to avoid unnecessary updates
if (state.current === newValue) return
// Update reference container instead of direct variable reassignment
state.current = newValue
// Notify all subscribers about the value change
if (subscribers.size > 0) {
subscribers.forEach(callback => {
tryCatch({
fn: () => callback(newValue),
onError: (error) => {
logger.error('Error in signal subscriber:', error)
}
})
})
}
},
/**
* Subscribe to value changes
* @param {Function} callback - Function to call when value changes
* @returns {() => void} Unsubscribe function
*/
subscribe(callback) {
if (!isFunction(callback)) {
throw new Error('[HookTML] Signal subscribers must be functions')
}
subscribers.add(callback)
// Return unsubscribe function
return () => {
subscribers.delete(callback)
}
},
destroy() {
// Clean up all subscribers
subscribers.clear()
},
// For debugging purposes
toString() {
return `Signal(${state.current})`
}
}
return signalObject
}