Thinking in Signals

A closer look at signal model

Tomasz Ducin  •      @tomasz_ducin  •  ducin.it

Hi, I'm Tomasz

Independent Consultant & Software Architect

Trainer, Speaker, JS/TS Expert

ArchitekturaNaFroncie.pl (ANF)

Warsaw, PL

tomasz (at) ducin.dev

@tomasz_ducin

let rate = 3.94;
let amount = 1000;
let exchange = amount / rate; // 253.80

rate = 3.97;
exchange // OUT OF SYNC!

IMPERATIVE

invariant

a condition which is true BEFORE and AFTER a change

REACTIVE w/RxJS

let rate$ = new BehaviorSubject(3.94);
let amount$ = new BehaviorSubject(1000);
let exchange$ = combineLatest(
  amount$, rate$, 
  (amount, rate) => amount / rate
);
rate$.next(3.97) // exchange -> 251.89

REACTIVE w/signals

let rate = signal(3.94);
let amount = signal(1000);
let exchange = computed(
   () => amount() / rate()); // 253.80
rate.set(3.97) // exchange() == 251.89

start modelling state with signals

Local State

@Component({...})
class MyComponent {
  private rate = signal(...)
  private amount = signal(...)
  public exchange = computed(...)
}

Shared State

@Injectable({...})
class MyService {
  private rate = signal(...)
  private amount = signal(...)
  public exchange = computed(...)
}
@Component({...})
class MyComponent {
  constructor(
    private myService: MyService
  ){}
}

Signal State Class (VM)

class MySignalState {
  private rate = signal(...)
  private amount = signal(...)
  public exchange = computed(...)
}
@Component({...})
class MyComponent {
  state = new MySignalState()
}
@Injectable({...})
class MyService {
  state = new MySignalState()
}

Signal State Machine

type LoadingState =
  | { type: "LOADING" }
  | { type: "DATA_LOADED", items: Entity[] }
  | { type: "ERROR", error: Error }

class MySignalState {
  #loadingSignal = signal<LoadingState>({ type: "LOADING" })

  public state = this.#loadingSignal.asReadonly()
}
<div>
  @if (state.type == "DATA_LOADED") {
    <app-entity-list [items]="state.items" />
  } @else if (state.type == "ERROR") {
    <app-error-messsage [message]="state.error.message" />
  } @else ...
</div>
sig = signal(t) // WritableSignal<T>

sig.set(t)
sig.update(oldT => newT)

sig.asReadonly() // Signal<T>

c = computed(() => sig())
e = effect(() => display(sig()))

// NO SUBSCRIBE, NO UNSUBSCRIBE

Signals API

dependencies

drive

reactivity

LAZY

REACTIVE

let's take a closer 🧐 at 🚦

no magic, just a function

reevaluate entire template

fine-grained CD:
DOM (Text)Node only

just PULLED
not reevaluated

reactivity
simplicity
testability

Static data

let rate = 3.94;
let amount = 1000;
let exchange = amount / rate; // 253.80

rate = 3.97;
exchange // OUT OF SYNC!

reactivity
simplicity
testability

Reactive Streams

let rate$ = new BehaviorSubject(3.94);
let amount$ = new BehaviorSubject(1000);
let exchange$ = combineLatest(
  amount$, rate$, 
  (amount, rate) => amount / rate
);
rate$.next(3.97) // exchange -> 251.89

reactivity
simplicity
testability

Signals

asynchronous

performance (fine-grained CD)

let rate = signal(3.94);
let amount = signal(1000);
let exchange = computed(
   () => amount() / rate()); // 253.80
rate.set(3.97) // exchange() == 251.89

start modelling state with signals

Thank you

Tomasz Ducin  •     @tomasz_ducin  •  ducin.it