All files / src/State State.ts

100% Statements 12/12
100% Branches 2/2
100% Functions 6/6
100% Lines 12/12

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 1129x 9x                                       9x                                   76x 76x                             223x       95x 93x 93x                     2x                                 9x                                     93x      
import { queueWatchers } from '../Compute'
import { Observable } from '../Observable'
 
/**
 * Reactive state that can be changed via `value` field with change tracking capability.
 *
 * @class State
 * @extends Observable
 * @template V - state value type
 * @param {V} [initial] - initial state value
 *
 * @example
 * // Create state
 * const count = new State(0)
 *
 * // Subscribe to changes
 * new Watch(() => console.log(count.value)) // logs: 0
 *
 * // Update value
 * count.value++ // logs: 1
 */
export class State<V = never | unknown> extends Observable<V extends never ? unknown : V> {
  /** Current value. No auto-subscription on direct access (unlike `value`). */
  raw: V extends never ? unknown : V
 
  /**
   * Initial state value set during construction.
   * Used by `reset()` to restore state to its original value.
   * Allows checking if the state has been modified.
   *
   * @example
   * const count = new State(0)
   *
 * const isChanged = count.initial === count.raw
   */
  readonly initial: V extends never ? unknown : V
 
  constructor (...args: V extends never | undefined ? [V?] : [V])
  constructor (initial?: any) {
    super()
    this.raw = this.initial = initial
  }
 
  /**
   * Current state value. Updates watchers only on actual changes (strict `!==`).
   * Using `value` inside a `Watch` callback automatically subscribes to changes.
   *
   * @example
   * new Watch(() => console.log(count.value)) // auto-subscribes to count
   *
   * @example
   * count.value = 1 // triggers watchers
   * count.value = 1 // no trigger
   */
  get value () {
    return super.value
  }
 
  set value (value: V extends never ? unknown : V) {
    if (this.raw !== value) {
      this.raw = value
      this.update()
    }
  }
 
  /**
   * Sets the state value. Identical to the `value` setter but returns `void`.
   * Useful as a shorthand in arrow functions: `() => state.set(value)` instead of `() => { state.value = value }`
   *
   * `state.set` cannot be used as a standalone function: `const set = state.set`
   */
  set (value: V extends never ? unknown : V) {
    this.value = value
  }
 
  /**
   * Resets state to its initial value.
   * Triggers watchers only if the current value differs from the initial value.
   *
   * @example
   * const count = new State(0)
   *
   * new Watch(() => console.log(count.value)) // logs: 0
   *
   * count.value = 5 // logs: 5
   *
   * count.reset() // logs: 0
   */
  reset () {
    this.value = this.initial
  }
 
  /**
   * Force triggers all watchers even if value didn't change.
   *
   * @example
   * // Create state
   * const log = new State([])
   *
   * // Subscribe to changes
   * new Watch(() => console.log(log.value)) // logs: []
   *
   * log.value.push(1) // no logs
   *
   * // Update value
   * count.update() // logs: [1]
   */
  update () {
    queueWatchers(this.observers)
  }
}