Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome back to our course on building a modern JavaScript framework.
In our last lesson, we created several factory functions (of, from, interval) to produce "cold," "unicast" observables. This means each time you subscribe, you get a new, independent execution of that observable's logic. This is great for many scenarios, but what if you want multiple parts of your application to listen to the exact same stream of values? Or what if you need to programmatically push values into a stream, much like an event emitter?
Today, we'll address this by implementing a Subject. The learning outcome for this lesson is to implement a generic Subject class that acts as both an Observer and an Observable to multicast values. A Subject is a special hybrid that sits at the heart of many reactive patterns, enabling you to share data streams and bridge imperative code with your reactive world.
1. From Unicast to Multicast: The Need for a Subject
Our current Observable is unicast. Imagine our interval(1000) observable. If two different parts of your code subscribe to it, each subscription will start its own independent setInterval timer. They will emit values in parallel, but they won't be synchronized.
Multicasting, by contrast, means a single observable execution is shared among multiple subscribers. When the source emits a value, all subscribers receive it. This is the core idea behind the Observer design pattern.

To achieve this, we need a Subject.
To get a formal definition, let's start with the official RxJS documentation. It clearly explains how a Subject is both an Observable and an Observer, which is the key to its multicasting ability.
Read the section 'What is a Subject?', specifically the core definitions. Focus on the two key statements: 'Every Subject is an Observable' and 'Every Subject is an Observer'. Note how the code example shows two observers receiving the same values from one subject.
Now, let's see a practical demonstration. The following video shows how a Subject is used in RxJS. The concepts are directly applicable to what we are building.
RxJS SUBJECT (~EventEmitter) | RxJS TUTORIAL
The video 'RxJS SUBJECT (~EventEmitter)' from Academind provides a great visual explanation of how to create a Subject, subscribe to it, and push values, errors, and completion notifications to multiple listeners.
Watch the Subject demo in the first four and a half minutes of the video. Pay attention to how subject.next(), subject.error(), and subject.complete() are called from outside the subscription, and how all subscribers react to these calls.
2. The Anatomy of a Subject
Based on the resources, we can define the structure of our Subject class. It's a fascinating hybrid that combines interfaces we've already built.
- It's an
Observable: It must have asubscribemethod so that consumers can listen to its values. We can achieve this by having ourSubjectclass extend ourObservableclass. - It's an
Observer: It must havenext(v),error(e), andcomplete()methods, allowing it to be on the receiving end of a stream. We'll implement ourObserverinterface. - It's a Multicaster: Internally, it needs to maintain a list of all the observers that have subscribed to it. When
next(),error(), orcomplete()is called on the subject, it will iterate through this list and call the corresponding method on each subscribed observer.
This UML diagram provides a clear visual of the relationships we are about to implement. Our Subject class will embody both the IObservable (by extending Observable) and IObserver roles.

3. Your Task: Implementing the Subject Class
Let's translate this design into code.
-
Create the file and class skeleton.
In yoursrcdirectory, create a new file namedsubject.ts. Inside, define theSubjectclass. It should extendObservable<T>and implement ourObserver<T>interface. It also needs an internal array to hold its observers and a flag to track its state.// src/subject.ts import { Observable } from './observable'; import { Observer } from './observer'; import { Subscription } from './subscription'; export class Subject<T> extends Observable<T> implements Observer<T> { private observers: Observer<T>[] = []; private isStopped = false; constructor() { // We call super with a producer that subscribes the subject to itself. // This allows `pipe` to work correctly. super(observer => { return this.subscribe(observer); }); } // Methods to be implemented... }Note: The
constructorhere is a bit of a mind-bender. By passingthis.subscribeto the parentObservableconstructor, we ensure that when someone uses thepipe()method on aSubject, the resulting newObservablecorrectly subscribes back to theSubjectitself. -
Implement the
Observerinterface methods.
These methods are the "input" side of theSubject. When they are called, theSubjectmulticasts the notification to all of its registered observers. We must also check theisStoppedflag to ensure no notifications are sent after an error or completion.// Inside the Subject<T> class in src/subject.ts public next(value: T): void { if (this.isStopped) { return; } // We create a copy of the observers array in case // a subscriber unsubscribes during the next() call. for (const observer of [...this.observers]) { observer.next(value); } } public error(err: any): void { if (this.isStopped) { return; } this.isStopped = true; for (const observer of [...this.observers]) { observer.error(err); } // Clear observers after termination this.observers = []; } public complete(): void { if (this.isStopped) { return; } this.isStopped = true; for (const observer of [...this.observers]) { observer.complete(); } // Clear observers after termination this.observers = []; } -
Override the
subscribemethod.
Thesubscribemethod from the parentObservableclass is not suitable because it expects a producer function. We need to override it to manage our internal list of observers. This method is the "output" side of theSubject.// Inside the Subject<T> class in src/subject.ts public subscribe(observer: Observer<T>): Subscription { if (this.isStopped) { // If the subject is already stopped, immediately notify // the new subscriber of the error or completion. // For simplicity, we'll just complete it. A more advanced // implementation might store the error. observer.complete(); return new Subscription(() => {}); } this.observers.push(observer); // Return a subscription that, when unsubscribed, // removes the observer from our list. const teardown = () => { const index = this.observers.indexOf(observer); if (index !== -1) { this.observers.splice(index, 1); } }; return new Subscription(teardown); }
4. Your Task: Testing the Subject
With the implementation complete, let's write tests to verify its behavior. Create a new test file src/subject.test.ts.
// src/subject.test.ts
import { describe, it, expect, vi } from 'vitest';
import { Subject } from './subject';
describe('Subject', () => {
it('should multicast values to multiple observers', () => {
const subject = new Subject<number>();
const observerA = { next: vi.fn(), error: vi.fn(), complete: vi.fn() };
const observerB = { next: vi.fn(), error: vi.fn(), complete: vi.fn() };
subject.subscribe(observerA);
subject.subscribe(observerB);
subject.next(1);
subject.next(2);
expect(observerA.next).toHaveBeenCalledTimes(2);
expect(observerA.next).toHaveBeenCalledWith(1);
expect(observerA.next).toHaveBeenCalledWith(2);
expect(observerB.next).toHaveBeenCalledTimes(2);
expect(observerB.next).toHaveBeenCalledWith(1);
expect(observerB.next).toHaveBeenCalledWith(2);
});
it('should stop multicasting after complete is called', () => {
const subject = new Subject<number>();
const observer = { next: vi.fn(), error: vi.fn(), complete: vi.fn() };
subject.subscribe(observer);
subject.next(1);
subject.complete();
subject.next(2); // This should be ignored
expect(observer.next).toHaveBeenCalledTimes(1);
expect(observer.next).toHaveBeenCalledWith(1);
expect(observer.complete).toHaveBeenCalledTimes(1);
});
it('should stop multicasting after error is called', () => {
const subject = new Subject<number>();
const observer = { next: vi.fn(), error: vi.fn(), complete: vi.fn() };
const testError = new Error('test');
subject.subscribe(observer);
subject.next(1);
subject.error(testError);
subject.next(2); // This should be ignored
expect(observer.next).toHaveBeenCalledTimes(1);
expect(observer.error).toHaveBeenCalledTimes(1);
expect(observer.error).toHaveBeenCalledWith(testError);
expect(observer.complete).not.toHaveBeenCalled();
});
it('should stop sending values to an observer after it unsubscribes', () => {
const subject = new Subject<number>();
const observerA = { next: vi.fn(), error: vi.fn(), complete: vi.fn() };
const observerB = { next: vi.fn(), error: vi.fn(), complete: vi.fn() };
const subA = subject.subscribe(observerA);
subject.subscribe(observerB);
subject.next(1);
subA.unsubscribe();
subject.next(2);
expect(observerA.next).toHaveBeenCalledTimes(1);
expect(observerA.next).toHaveBeenCalledWith(1);
expect(observerB.next).toHaveBeenCalledTimes(2);
expect(observerB.next).toHaveBeenCalledWith(1);
expect(observerB.next).toHaveBeenCalledWith(2);
});
});
Run your tests to confirm that your Subject behaves as expected.
Conclusion
Congratulations! You have successfully implemented a Subject, a cornerstone of reactive programming. You've created a class that is both an Observable and an Observer, enabling it to multicast values to many subscribers.
Key Takeaways:
- Subject as a Hybrid: A
Subjectuniquely combines theObservableandObserverinterfaces. - Multicasting: Unlike unicast observables, a
Subjectshares a single execution among all its subscribers. - Imperative Bridge: Subjects allow you to imperatively call
.next(),.error(), and.complete(), acting as a bridge between traditional event-driven code and the reactive world. - State Management: A
Subjectmust manage its internal state, keeping a list of observers and tracking whether it has been stopped to ensure correct behavior.
Preview of the Next Lesson:
Now that we have a Subject, we can unlock one of its most powerful use cases: sharing a single subscription to a cold observable. In the next lesson, we will implement a share operator. This operator will use a Subject internally to convert any cold, unicast observable into a hot, multicast one, preventing redundant work and synchronizing data for all consumers.