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:
- Understand the Observable pattern and its relationship to functional programming
- Implement a basic Observable/Stream type from scratch
- Build operators (map, filter, merge, debounce, switchMap)
- Create reactive state management
- Build a simple virtual DOM or reactive binding system
- See how all previous FP concepts culminate in reactive systems
- 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 existEither<E, A>: A value or an errorIO<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
- Observable Core:
Observable<A>interface with subscribeObserver<A>interface (next, error, complete)Subscriptionwith unsubscribe
- Creation Operators:
of(...values): From valuesfrom(iterable): From array/iterablefromEvent(element, eventName): From DOM eventsinterval(ms): Periodic valuestimer(delay): Single delayed valueSubject: Hot observable you can push to
- Transformation Operators:
map(fn): Transform valuesfilter(pred): Filter valuesscan(reducer, seed): Accumulate (like reduce)flatMap/mergeMap(fn): Map to observable, flattenswitchMap(fn): Map to observable, cancel previous
- Combination Operators:
merge(...streams): Combine multiple streamscombineLatest(...streams): Latest from eachwithLatestFrom(other): Main stream with latest from otherzip(...streams): Pair up by index
- Utility Operators:
debounceTime(ms): Wait for pausethrottleTime(ms): Limit ratetake(n): First n valuestakeUntil(notifier): Until notifier emitsdistinctUntilChanged(): Skip duplicatesstartWith(value): Prepend value
- State Management:
BehaviorSubject: Subject with current valuestore(reducer, initialState): Redux-like store- Action dispatching
- 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
ofandfromsources
Tasks:
- Create
Observer<A>interface - Create
Subscriptioninterface with unsubscribe - Create
createObservablefactory function - Implement
pipefor chaining operators - Implement
of(...values)source - 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:
- Implement
map(fn)operator - Implement
filter(pred)operator - Implement
scan(reducer, seed)operator - Implement
take(n)operator - Implement
takeUntil(notifier)operator - 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:
- Implement
Subject<A>- hot observable - Implement
BehaviorSubject<A>- with current value - Implement
fromEvent(element, eventName) - Implement
interval(ms) - 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:
- Implement
merge(...observables)- combine streams - Implement
combineLatest(...observables)- latest from each - Implement
switchMap(fn)- map and cancel previous - Implement
debounceTime(ms)- wait for pause - 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:
- Implement
createStore(reducer, initialState) - Connect to Subject for state stream
- Implement dispatch method
- 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:
- Create HTML structure for demos
- Build counter with increment/decrement
- Build todo with add/toggle/filter
- Demonstrate debounced search
- 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
- 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]);
});
- 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
9.2 Related Open Source Projects
- 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
10.4 Related Projects in This Series
- 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.