Project 8: Functional Reactive UI

Project 8: Functional Reactive UI

Build a reactive UI framework where events are streams you can map, filter, and compose.

Quick Reference

Attribute Value
Difficulty Master
Time Estimate 2-3 Weeks
Language TypeScript
Prerequisites Projects 1-7, DOM manipulation, Basic RxJS concepts
Key Topics Observables, Event Streams, State Management, Virtual DOM

1. Learning Objectives

By completing this project, you will:

  1. Understand the Observable pattern and its relationship to functional programming
  2. Implement a basic Observable/Stream type from scratch
  3. Build operators (map, filter, merge, debounce, switchMap)
  4. Create reactive state management
  5. Build a simple virtual DOM or reactive binding system
  6. See how all previous FP concepts culminate in reactive systems
  7. Understand the architecture behind RxJS, Cycle.js, and React

2. Theoretical Foundation

2.1 Core Concepts

From Values to Streams

Weโ€™ve learned about containers for single values:

  • Maybe<A>: A value that might not exist
  • Either<E, A>: A value or an error
  • IO<A>: A deferred computation

What about multiple values over time?

Single value:        42
Array (multiple):    [1, 2, 3]
Promise (async):     eventually 42
Observable (stream): 1 ... 2 ... 3 ... over time

Observables represent sequences of values delivered over time:

Regular array: [1, 2, 3] โ† All values exist now

Observable timeline:
Time โ†’  t0    t1    t2    t3    t4    ...
         โ”‚     โ”‚     โ”‚     โ”‚     โ”‚
         1     2     3     โœ“     โœ—
         โ†‘     โ†‘     โ†‘     โ†‘     โ†‘
      value  value  value  complete  error

The Observable Type

An Observable is a function that takes an Observer and sets up the data source:

// Observer: handles values, errors, completion
interface Observer<A> {
  next: (value: A) => void;
  error: (err: Error) => void;
  complete: () => void;
}

// Observable: produces values when subscribed
interface Observable<A> {
  subscribe: (observer: Observer<A>) => Subscription;
}

interface Subscription {
  unsubscribe: () => void;
}

// Create an observable
const numbers: Observable<number> = {
  subscribe: (observer) => {
    observer.next(1);
    observer.next(2);
    observer.next(3);
    observer.complete();
    return { unsubscribe: () => {} };
  }
};

// Subscribe to it
numbers.subscribe({
  next: (x) => console.log(x),    // 1, 2, 3
  error: (e) => console.error(e),
  complete: () => console.log('done')
});

Events as Streams

DOM events become observable streams:

// Traditional: callback-based
button.addEventListener('click', (event) => {
  console.log('clicked', event);
});

// Reactive: stream of click events
const clicks: Observable<MouseEvent> = fromEvent(button, 'click');

// Now you can compose!
const doubleClicks = clicks.pipe(
  bufferTime(300),
  filter(events => events.length >= 2)
);

Operators: Stream Transformations

Operators transform streams, returning new streams:

// Map: Transform each value
const doubled = numbers.pipe(
  map(x => x * 2)
);
// 1...2...3 โ†’ 2...4...6

// Filter: Keep matching values
const evens = numbers.pipe(
  filter(x => x % 2 === 0)
);
// 1...2...3...4 โ†’ 2...4

// Merge: Combine streams
const combined = merge(stream1, stream2);
//  stream1:  --1----3----5-
//  stream2:  ----2----4----
//  combined: --1-2--3-4--5-

// SwitchMap: Map to stream, cancel previous
const search = input.pipe(
  debounceTime(300),
  switchMap(term => http.get(`/search?q=${term}`))
);

Visual representation:

Map Operator:
โ”€1โ”€โ”€2โ”€โ”€3โ”€โ”€โ–ถ
   โ”‚map(x => x * 2)
   โ–ผ
โ”€2โ”€โ”€4โ”€โ”€6โ”€โ”€โ–ถ

Filter Operator:
โ”€1โ”€โ”€2โ”€โ”€3โ”€โ”€4โ”€โ”€โ–ถ
   โ”‚filter(x => x % 2 === 0)
   โ–ผ
โ”€โ”€โ”€โ”€2โ”€โ”€โ”€โ”€โ”€4โ”€โ”€โ–ถ

Merge Operator:
โ”€Aโ”€โ”€โ”€โ”€Bโ”€โ”€โ”€โ”€Cโ”€โ”€โ–ถ stream1
โ”€โ”€โ”€1โ”€โ”€โ”€โ”€2โ”€โ”€โ”€โ”€โ”€โ–ถ stream2
         โ”‚merge
         โ–ผ
โ”€Aโ”€1โ”€โ”€Bโ”€2โ”€Cโ”€โ”€โ”€โ–ถ merged

Reactive State Management

State becomes a stream that updates over time:

// State as a stream
interface State {
  count: number;
  loading: boolean;
}

// Actions as a stream
type Action =
  | { type: 'INCREMENT' }
  | { type: 'DECREMENT' }
  | { type: 'SET_LOADING'; loading: boolean };

// Reducer: combine state and action
const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case 'INCREMENT': return { ...state, count: state.count + 1 };
    case 'DECREMENT': return { ...state, count: state.count - 1 };
    case 'SET_LOADING': return { ...state, loading: action.loading };
  }
};

// State stream from actions
const actions$ = new Subject<Action>();
const state$ = actions$.pipe(
  scan(reducer, initialState),
  startWith(initialState)
);

// Subscribe to render
state$.subscribe(state => render(state));

// Dispatch actions
actions$.next({ type: 'INCREMENT' });

This is the core of Redux, but stream-based!

Virtual DOM / Reactive Rendering

Connect state streams to UI:

// State stream
const state$: Observable<State> = ...;

// View function: State โ†’ VirtualDOM
const view = (state: State): VNode => (
  h('div', {}, [
    h('p', {}, [`Count: ${state.count}`]),
    h('button', { onclick: () => dispatch({ type: 'INCREMENT' }) }, ['+']),
    h('button', { onclick: () => dispatch({ type: 'DECREMENT' }) }, ['-'])
  ])
);

// Render on every state change
state$.pipe(
  map(view),
  scan(patch, initialDOM)  // Diff and patch
).subscribe();

The reactive loop:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    Reactive Loop                         โ”‚
โ”‚                                                         โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”‚
โ”‚   โ”‚  Events  โ”‚โ”€โ”€โ”€โ–ถโ”‚ Actions  โ”‚โ”€โ”€โ”€โ–ถโ”‚  State   โ”‚         โ”‚
โ”‚   โ”‚ (clicks) โ”‚    โ”‚ (stream) โ”‚    โ”‚ (stream) โ”‚         โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜         โ”‚
โ”‚                                         โ”‚               โ”‚
โ”‚        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜               โ”‚
โ”‚        โ–ผ                                                โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”‚
โ”‚   โ”‚   View   โ”‚โ”€โ”€โ”€โ–ถโ”‚   VDOM   โ”‚โ”€โ”€โ”€โ–ถโ”‚   DOM    โ”‚         โ”‚
โ”‚   โ”‚ function โ”‚    โ”‚  (diff)  โ”‚    โ”‚ (render) โ”‚         โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜         โ”‚
โ”‚                                         โ”‚               โ”‚
โ”‚        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜               โ”‚
โ”‚                    (more events)                        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

2.2 Why This Matters

In Modern Frameworks:

  • RxJS: Core of Angular, powers NgRx
  • React Hooks: useState/useEffect are simplified subscriptions
  • Redux-Observable: Middleware using observables
  • Cycle.js: Fully reactive framework
  • MobX: Observable-based state

Benefits:

  • Composable event handling
  • Declarative async logic
  • Automatic resource cleanup
  • Time-based operations (debounce, throttle)

2.3 Historical Context

  • 1997: Functional Reactive Programming (FRP) in Haskell
  • 2009: Microsoft Rx (Reactive Extensions) for .NET
  • 2012: RxJS for JavaScript
  • 2013: React introduced component-based reactive thinking
  • 2015: Redux combined functional state with React
  • 2016: Cycle.js showed pure functional reactive UI
  • Now: Every major framework has reactive patterns

2.4 Common Misconceptions

โ€œObservables are just Promisesโ€

  • Reality: Observables handle multiple values, Promises handle one
  • Observables are lazy; Promises execute immediately
  • Observables can be canceled; Promises cannot

โ€œRxJS is too complexโ€

  • Reality: You donโ€™t need all operators
  • map, filter, merge, switchMap cover 80% of use cases

โ€œReactive is only for UIsโ€

  • Reality: Server-side streams, IoT, data processing all use reactive

3. Project Specification

3.1 What You Will Build

A minimal but complete reactive UI framework:

  • Observable<A> type with core operators
  • DOM event helpers (fromEvent)
  • Reactive state management
  • Simple virtual DOM or direct DOM bindings
  • A working demo application (todo app or counter)

3.2 Functional Requirements

  1. Observable Core:
    • Observable<A> interface with subscribe
    • Observer<A> interface (next, error, complete)
    • Subscription with unsubscribe
  2. Creation Operators:
    • of(...values): From values
    • from(iterable): From array/iterable
    • fromEvent(element, eventName): From DOM events
    • interval(ms): Periodic values
    • timer(delay): Single delayed value
    • Subject: Hot observable you can push to
  3. Transformation Operators:
    • map(fn): Transform values
    • filter(pred): Filter values
    • scan(reducer, seed): Accumulate (like reduce)
    • flatMap/mergeMap(fn): Map to observable, flatten
    • switchMap(fn): Map to observable, cancel previous
  4. Combination Operators:
    • merge(...streams): Combine multiple streams
    • combineLatest(...streams): Latest from each
    • withLatestFrom(other): Main stream with latest from other
    • zip(...streams): Pair up by index
  5. Utility Operators:
    • debounceTime(ms): Wait for pause
    • throttleTime(ms): Limit rate
    • take(n): First n values
    • takeUntil(notifier): Until notifier emits
    • distinctUntilChanged(): Skip duplicates
    • startWith(value): Prepend value
  6. State Management:
    • BehaviorSubject: Subject with current value
    • store(reducer, initialState): Redux-like store
    • Action dispatching
  7. DOM Integration:
    • bind(element, observable): Bind observable to element
    • Simple rendering or virtual DOM

3.3 Non-Functional Requirements

  • Lazy: Observables donโ€™t run until subscribed
  • Cleanup: Subscriptions properly dispose resources
  • Memory Safe: No memory leaks from forgotten subscriptions
  • Type Safe: Full generic types

3.4 Example Usage / Output

// Create observables
const clicks = fromEvent(document, 'click');
const input = fromEvent(searchBox, 'input');

// Transform and combine
const searchResults = input.pipe(
  map(e => e.target.value),
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(term =>
    term.length < 2
      ? of([])
      : fetchSearchResults(term)
  )
);

// Subscribe and render
searchResults.subscribe({
  next: results => renderResults(results),
  error: err => showError(err)
});

// State management
interface TodoState {
  todos: Todo[];
  filter: 'all' | 'active' | 'completed';
}

type TodoAction =
  | { type: 'ADD_TODO'; text: string }
  | { type: 'TOGGLE_TODO'; id: number }
  | { type: 'SET_FILTER'; filter: TodoState['filter'] };

const store = createStore(todoReducer, initialState);

// React to state changes
store.state$.pipe(
  map(state => state.todos.filter(filterFn(state.filter)))
).subscribe(todos => renderTodos(todos));

// Dispatch actions
addButton.addEventListener('click', () => {
  store.dispatch({ type: 'ADD_TODO', text: input.value });
});

// Complete reactive component
const Counter = () => {
  const count$ = new BehaviorSubject(0);

  const increment$ = fromEvent(incBtn, 'click').pipe(map(() => 1));
  const decrement$ = fromEvent(decBtn, 'click').pipe(map(() => -1));

  merge(increment$, decrement$).pipe(
    scan((count, delta) => count + delta, 0)
  ).subscribe(count$);

  count$.subscribe(count => {
    display.textContent = `Count: ${count}`;
  });
};

4. Solution Architecture

4.1 High-Level Design

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                  Reactive UI Framework                      โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                            โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚              Observable Core                          โ”‚  โ”‚
โ”‚  โ”‚  Observable<A>, Observer<A>, Subscription             โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                          โ”‚                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚               Operators                               โ”‚  โ”‚
โ”‚  โ”‚  map, filter, scan, merge, switchMap, debounce        โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                          โ”‚                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚            Event Sources                              โ”‚  โ”‚
โ”‚  โ”‚  fromEvent, interval, Subject, BehaviorSubject        โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                          โ”‚                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚              State Management                         โ”‚  โ”‚
โ”‚  โ”‚  createStore, reducer pattern, action dispatch        โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                          โ”‚                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚              DOM Integration                          โ”‚  โ”‚
โ”‚  โ”‚  Reactive bindings or simple virtual DOM              โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                                                            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

4.2 Key Components

Component Responsibility Key Decisions
Observable<A> Core stream type Function-based, lazy
Operators Transform streams Pipeable, return new observables
Subject Hot observable Multicast to subscribers
BehaviorSubject Subject with value Stores current value
Store State management scan + Subject pattern
DOM helpers Event bindings fromEvent, bind

4.3 Data Structures

// Observer: receives values
interface Observer<A> {
  next: (value: A) => void;
  error: (err: Error) => void;
  complete: () => void;
}

// Subscription: can be canceled
interface Subscription {
  unsubscribe: () => void;
  closed: boolean;
}

// Observable: lazy stream
type Observable<A> = {
  subscribe: (observer: Partial<Observer<A>>) => Subscription;
  pipe: <B>(...operators: Array<(obs: Observable<any>) => Observable<any>>) => Observable<B>;
};

// Subject: observable you can push to
interface Subject<A> extends Observable<A> {
  next: (value: A) => void;
  error: (err: Error) => void;
  complete: () => void;
}

// BehaviorSubject: subject with current value
interface BehaviorSubject<A> extends Subject<A> {
  getValue: () => A;
}

// Operator: transforms observables
type Operator<A, B> = (source: Observable<A>) => Observable<B>;

// Store: state management
interface Store<S, A> {
  state$: Observable<S>;
  dispatch: (action: A) => void;
  getState: () => S;
}

4.4 Algorithm Overview

Observable Factory:

const createObservable = <A>(
  subscribe: (observer: Observer<A>) => (() => void) | void
): Observable<A> => ({
  subscribe: (partialObserver) => {
    const observer: Observer<A> = {
      next: partialObserver.next || (() => {}),
      error: partialObserver.error || ((e) => { throw e; }),
      complete: partialObserver.complete || (() => {})
    };

    let closed = false;
    const cleanup = subscribe(observer);

    return {
      unsubscribe: () => {
        if (!closed) {
          closed = true;
          cleanup?.();
        }
      },
      get closed() { return closed; }
    };
  },
  pipe: (...operators) => operators.reduce((obs, op) => op(obs), this)
});

Map Operator:

const map = <A, B>(fn: (a: A) => B): Operator<A, B> =>
  (source) => createObservable((observer) => {
    const subscription = source.subscribe({
      next: (value) => observer.next(fn(value)),
      error: (err) => observer.error(err),
      complete: () => observer.complete()
    });
    return () => subscription.unsubscribe();
  });

SwitchMap Operator:

const switchMap = <A, B>(fn: (a: A) => Observable<B>): Operator<A, B> =>
  (source) => createObservable((observer) => {
    let innerSubscription: Subscription | null = null;

    const subscription = source.subscribe({
      next: (value) => {
        innerSubscription?.unsubscribe();
        innerSubscription = fn(value).subscribe({
          next: (inner) => observer.next(inner),
          error: (err) => observer.error(err)
        });
      },
      error: (err) => observer.error(err),
      complete: () => observer.complete()
    });

    return () => {
      innerSubscription?.unsubscribe();
      subscription.unsubscribe();
    };
  });

5. Implementation Guide

5.1 Development Environment Setup

mkdir reactive-ui && cd reactive-ui
npm init -y
npm install --save-dev typescript ts-node parcel @types/node
npx tsc --init
mkdir src tests examples
mkdir src/{core,operators,sources,state,dom}

Add to package.json:

{
  "scripts": {
    "dev": "parcel examples/index.html",
    "build": "tsc",
    "test": "jest"
  }
}

5.2 Project Structure

reactive-ui/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ core/
โ”‚   โ”‚   โ”œโ”€โ”€ observable.ts    # Observable type and factory
โ”‚   โ”‚   โ”œโ”€โ”€ observer.ts      # Observer interface
โ”‚   โ”‚   โ”œโ”€โ”€ subscription.ts  # Subscription handling
โ”‚   โ”‚   โ””โ”€โ”€ index.ts
โ”‚   โ”œโ”€โ”€ operators/
โ”‚   โ”‚   โ”œโ”€โ”€ map.ts
โ”‚   โ”‚   โ”œโ”€โ”€ filter.ts
โ”‚   โ”‚   โ”œโ”€โ”€ scan.ts
โ”‚   โ”‚   โ”œโ”€โ”€ merge.ts
โ”‚   โ”‚   โ”œโ”€โ”€ switchMap.ts
โ”‚   โ”‚   โ”œโ”€โ”€ debounceTime.ts
โ”‚   โ”‚   โ””โ”€โ”€ index.ts
โ”‚   โ”œโ”€โ”€ sources/
โ”‚   โ”‚   โ”œโ”€โ”€ of.ts            # From values
โ”‚   โ”‚   โ”œโ”€โ”€ fromEvent.ts     # From DOM events
โ”‚   โ”‚   โ”œโ”€โ”€ interval.ts      # Periodic
โ”‚   โ”‚   โ”œโ”€โ”€ subject.ts       # Subject
โ”‚   โ”‚   โ”œโ”€โ”€ behaviorSubject.ts
โ”‚   โ”‚   โ””โ”€โ”€ index.ts
โ”‚   โ”œโ”€โ”€ state/
โ”‚   โ”‚   โ”œโ”€โ”€ store.ts         # createStore
โ”‚   โ”‚   โ””โ”€โ”€ index.ts
โ”‚   โ”œโ”€โ”€ dom/
โ”‚   โ”‚   โ”œโ”€โ”€ bind.ts          # Bind to DOM
โ”‚   โ”‚   โ””โ”€โ”€ index.ts
โ”‚   โ””โ”€โ”€ index.ts             # Public API
โ”œโ”€โ”€ tests/
โ”‚   โ”œโ”€โ”€ observable.test.ts
โ”‚   โ”œโ”€โ”€ operators.test.ts
โ”‚   โ””โ”€โ”€ state.test.ts
โ”œโ”€โ”€ examples/
โ”‚   โ”œโ”€โ”€ index.html
โ”‚   โ”œโ”€โ”€ counter.ts           # Counter demo
โ”‚   โ””โ”€โ”€ todo.ts              # Todo app demo
โ””โ”€โ”€ package.json

5.3 Implementation Phases

Phase 1: Observable Core (2-3 days)

Goals:

  • Implement Observable, Observer, Subscription
  • Implement pipe method
  • Create of and from sources

Tasks:

  1. Create Observer<A> interface
  2. Create Subscription interface with unsubscribe
  3. Create createObservable factory function
  4. Implement pipe for chaining operators
  5. Implement of(...values) source
  6. Implement from(iterable) source

Checkpoint:

const obs = of(1, 2, 3);
const values: number[] = [];
obs.subscribe({ next: v => values.push(v) });
expect(values).toEqual([1, 2, 3]);

Phase 2: Core Operators (3-4 days)

Goals:

  • Implement essential operators
  • Ensure proper cleanup

Tasks:

  1. Implement map(fn) operator
  2. Implement filter(pred) operator
  3. Implement scan(reducer, seed) operator
  4. Implement take(n) operator
  5. Implement takeUntil(notifier) operator
  6. Test cleanup on unsubscribe

Checkpoint:

const result = of(1, 2, 3, 4, 5).pipe(
  filter(x => x % 2 === 1),
  map(x => x * 10),
  take(2)
);

const values: number[] = [];
result.subscribe({ next: v => values.push(v) });
expect(values).toEqual([10, 30]);

Phase 3: Subjects & Event Sources (2-3 days)

Goals:

  • Implement Subject (multicast)
  • Implement BehaviorSubject
  • Implement fromEvent

Tasks:

  1. Implement Subject<A> - hot observable
  2. Implement BehaviorSubject<A> - with current value
  3. Implement fromEvent(element, eventName)
  4. Implement interval(ms)
  5. Test multicast behavior

Checkpoint:

const subject = new Subject<number>();
const values1: number[] = [];
const values2: number[] = [];

subject.subscribe({ next: v => values1.push(v) });
subject.next(1);
subject.subscribe({ next: v => values2.push(v) });
subject.next(2);

expect(values1).toEqual([1, 2]);
expect(values2).toEqual([2]);  // Only received after subscribing

Phase 4: Advanced Operators (2-3 days)

Goals:

  • Implement merge, combineLatest
  • Implement switchMap
  • Implement debounceTime

Tasks:

  1. Implement merge(...observables) - combine streams
  2. Implement combineLatest(...observables) - latest from each
  3. Implement switchMap(fn) - map and cancel previous
  4. Implement debounceTime(ms) - wait for pause
  5. Implement distinctUntilChanged()

Checkpoint:

const source = new Subject<string>();
const results: string[] = [];

source.pipe(
  debounceTime(100),
  distinctUntilChanged()
).subscribe({ next: v => results.push(v) });

source.next('a');
source.next('a');  // Duplicate
await delay(150);
source.next('b');
await delay(150);

expect(results).toEqual(['a', 'b']);

Phase 5: State Management (1-2 days)

Goals:

  • Create store pattern
  • Action dispatching
  • State stream

Tasks:

  1. Implement createStore(reducer, initialState)
  2. Connect to Subject for state stream
  3. Implement dispatch method
  4. Test state updates

Checkpoint:

const store = createStore(
  (state, action) => {
    switch (action.type) {
      case 'INCREMENT': return { count: state.count + 1 };
      default: return state;
    }
  },
  { count: 0 }
);

const states: number[] = [];
store.state$.subscribe(s => states.push(s.count));

store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });

expect(states).toEqual([0, 1, 2]);

Phase 6: Demo Application (2-3 days)

Goals:

  • Build working counter app
  • Build todo app
  • Show complete reactive loop

Tasks:

  1. Create HTML structure for demos
  2. Build counter with increment/decrement
  3. Build todo with add/toggle/filter
  4. Demonstrate debounced search
  5. Document the demo apps

Checkpoint:

  • Counter works with reactive state
  • Todo app with all features
  • Search with debounce shows results

5.4 Key Implementation Decisions

Decision Options Recommendation Rationale
Observable type Class vs Function Function Simpler, more FP-style
Pipe implementation Method vs Function Method Better ergonomics
Subject implementation Extends Observable Separate Clearer semantics
State management Custom vs Redux-like Redux-like Familiar pattern

6. Testing Strategy

6.1 Test Categories

Category Purpose Examples
Core Observable lifecycle Subscribe, unsubscribe, complete
Operators Correct transformations map, filter output
Async Timing-based operators debounce, throttle
Integration End-to-end State to DOM updates

6.2 Critical Test Cases

  1. Lazy Subscription: ```typescript test(โ€˜observable does not emit until subscribedโ€™, () => { let emitted = false; const obs = createObservable((observer) => { emitted = true; observer.next(1); });

expect(emitted).toBe(false); obs.subscribe({}); expect(emitted).toBe(true); });


2. **Cleanup on Unsubscribe:**
```typescript
test('unsubscribe stops receiving values', () => {
  const subject = new Subject<number>();
  const values: number[] = [];

  const sub = subject.subscribe({ next: v => values.push(v) });
  subject.next(1);
  sub.unsubscribe();
  subject.next(2);

  expect(values).toEqual([1]);
});
  1. SwitchMap Cancels Previous: ```typescript test(โ€˜switchMap cancels previous inner observableโ€™, async () => { const source = new Subject(); const results: string[] = [];

source.pipe( switchMap(x => of(result-${x}).pipe(delay(50))) ).subscribe({ next: v => results.push(v) });

source.next(1); source.next(2); // Should cancel result-1 await delay(100);

expect(results).toEqual([โ€˜result-2โ€™]); });


### 6.3 Test Data

```typescript
// Test observables
const sync$ = of(1, 2, 3);
const async$ = interval(10).pipe(take(3));
const subject$ = new Subject<string>();

// Test helpers
const collectValues = <A>(obs: Observable<A>): Promise<A[]> => {
  const values: A[] = [];
  return new Promise((resolve) => {
    obs.subscribe({
      next: v => values.push(v),
      complete: () => resolve(values)
    });
  });
};

7. Common Pitfalls & Debugging

7.1 Frequent Mistakes

Pitfall Symptom Solution
Forgetting to unsubscribe Memory leaks Use takeUntil or explicit cleanup
Cold vs Hot confusion Unexpected behavior Understand Subject vs Observable
Missing error handling Silent failures Always provide error handler
Not handling completion Stream stops unexpectedly Handle complete callback

7.2 Debugging Strategies

// Tap operator for debugging
const tap = <A>(fn: (a: A) => void): Operator<A, A> =>
  (source) => createObservable((observer) => {
    return source.subscribe({
      next: (value) => {
        fn(value);
        observer.next(value);
      },
      error: observer.error,
      complete: observer.complete
    }).unsubscribe;
  });

// Usage
clicks.pipe(
  tap(e => console.log('Click:', e)),
  map(e => ({ x: e.clientX, y: e.clientY })),
  tap(pos => console.log('Position:', pos))
).subscribe(...);

7.3 Performance Traps

  • Creating observables in subscribe: Leads to subscription explosion
  • Not using share/multicast: Multiple subscriptions run separately
  • Heavy operations in operators: Block the event loop

8. Extensions & Challenges

8.1 Beginner Extensions

  • Buffer: Collect values into arrays
  • Delay: Delay emissions
  • Retry: Retry on error

8.2 Intermediate Extensions

  • Share/Multicast: Share subscription
  • Virtual DOM: Implement simple VDOM diffing
  • Router: URL-based routing with observables

8.3 Advanced Extensions

  • Scheduler: Control execution timing
  • Testing utilities: Virtual time for tests
  • DevTools: Observable inspector

9. Real-World Connections

9.1 Industry Applications

  • RxJS: Power behind Angular
  • Redux-Observable: Async middleware
  • Cycle.js: Fully reactive framework
  • MobX: Observable state management
  • RxJS: https://rxjs.dev - The reference implementation
  • xstream: https://github.com/staltz/xstream - Simpler alternative
  • Cycle.js: https://cycle.js.org - Pure functional reactive

9.3 Interview Relevance

  • โ€œImplement debounceโ€: Common question
  • โ€œExplain reactive programmingโ€: Show understanding
  • โ€œHow does RxJS work?โ€: You can explain from implementation

10. Resources

10.1 Essential Reading

  • โ€œReactive Design Patternsโ€ by Roland Kuhn - Reactive principles
  • โ€œRxJS in Actionโ€ by Paul Daniels - Practical RxJS
  • โ€œHands-On Functional Programming with TypeScriptโ€ Ch. 9 - Reactive FP

10.2 Video Resources

  • โ€œLearning Observable By Building Observableโ€ - Ben Lesh (YouTube)
  • โ€œThe introduction to Reactive Programming youโ€™ve been missingโ€ - Andrรฉ Staltz

10.3 Tools & Documentation

  • RxJS Docs: https://rxjs.dev/guide/overview
  • RxMarbles: https://rxmarbles.com - Visual operator diagrams
  • Learn RxJS: https://www.learnrxjs.io - Operator examples
  • Previous Projects: All of them! This project integrates everything
  • Capstone: Declarative Spreadsheet Engine

11. Self-Assessment Checklist

Before considering this project complete, verify:

Understanding

  • I can explain cold vs hot observables
  • I understand how operators create new observables
  • I can explain the reactive loop
  • I understand why cleanup/unsubscribe matters

Implementation

  • Observable core works correctly
  • All essential operators work
  • Subject and BehaviorSubject work
  • fromEvent integrates with DOM
  • State management works
  • Demo app runs correctly

Growth

  • I can build reactive UIs without RxJS
  • I understand RxJS at a deeper level
  • I can debug reactive code effectively

12. Submission / Completion Criteria

Minimum Viable Completion:

  • Observable core with subscribe, pipe
  • map, filter, scan operators
  • Subject working
  • Simple counter demo

Full Completion:

  • All operators implemented
  • fromEvent and DOM integration
  • State management with store
  • Todo app demo working
  • Comprehensive tests

Excellence (Going Above & Beyond):

  • Virtual DOM implementation
  • DevTools or debugging utilities
  • Published as npm package
  • Comparison with RxJS

This guide was generated from FUNCTIONAL_PROGRAMMING_TYPESCRIPT_LEARNING_PROJECTS.md. For the complete learning path, see the parent directory.