RxJS Subjects are a powerful feature that acts as both an Observable and an Observer, allowing you to multicast values to multiple subscribers. In this article, we’ll explore the three main types of Subjects with detailed code examples that you can run and understand.
Subject
A Subject is like a bridge or proxy that can act as both an Observable (you can subscribe to it) and an Observer (you can send values to it). Think of it as a radio station that can both broadcast messages and receive them.
#Key Characteristics of Subject:
- Hot Observable: Emits values regardless of whether there are subscribers
- Multicasting: One source can emit to multiple subscribers
- No initial value: New subscribers don’t receive previously emitted values
- Stateless: Doesn’t store any values
// Basic Subject - no initial value needed
let subject = new Subject<string>();
// First subscriber
subject.subscribe((data) => {
console.log("First subscriber got data >>>>> " + data);
});
// Emit first value
subject.next("First value");
// Second subscriber (added AFTER first emission)
subject.subscribe((data) => {
console.log("Second subscriber got data >>>>> " + data);
});
// Emit second value
subject.next("Second value");
// Console result:
// First subscriber got data >>>>> First value
// First subscriber got data >>>>> Second value
// Second subscriber got data >>>>> Second value
Key Observation: The second subscriber only receives “Second value” because it subscribed after “First value” was emitted.
#Use Cases for Subject:
- Event broadcasting: Button clicks, form submissions
- Real-time updates: Live notifications, websocket messages
- Component communication: When you don’t need to store state
BehaviorSubject
A BehaviorSubject is like a Subject with memory. It always holds the latest value and immediately emits it to new subscribers.
#Key Characteristics of BehaviorSubject:
- Requires initial value: Must be created with a default value
- Stores current value: Always remembers the last emitted value
- Immediate emission: New subscribers instantly receive the current value
- Stateful: Maintains the current state
// Behavior subjects need a first value
let subject = new BehaviorSubject<string>("First value");
// First subscriber
subject.asObservable().subscribe((data) => {
console.log("First subscriber got data >>>>> " + data);
});
subject.next("Second value");
// Second subscriber (added AFTER emissions)
subject.asObservable().subscribe((data) => {
console.log("Second subscriber got data >>>>> " + data);
});
subject.next("Third value");
// Console result:
// First subscriber got data >>>>> First value
// First subscriber got data >>>>> Second value
// Second subscriber got data >>>>> Second value (immediately gets current value!)
// First subscriber got data >>>>> Third value
// Second subscriber got data >>>>> Third value
Key Observation: The second subscriber immediately receives “Second value” (the current state) when it subscribes, even though it missed the initial value and the transition.
#Advanced BehaviorSubject Example:
// User authentication state management
let userState = new BehaviorSubject<{isLoggedIn: boolean, username: string}>({
isLoggedIn: false,
username: ''
});
// Component 1 subscribes
userState.subscribe(state => {
console.log("Component 1 - User state:", state);
});
// User logs in
userState.next({isLoggedIn: true, username: 'john_doe'});
// Component 2 subscribes later (like a new page loading)
userState.subscribe(state => {
console.log("Component 2 - User state:", state);
});
// Get current value without subscribing
console.log("Current user state:", userState.getValue());
// Console result:
// Component 1 - User state: {isLoggedIn: false, username: ''}
// Component 1 - User state: {isLoggedIn: true, username: 'john_doe'}
// Component 2 - User state: {isLoggedIn: true, username: 'john_doe'} (immediate!)
// Current user state: {isLoggedIn: true, username: 'john_doe'}
#Use Cases for BehaviorSubject:
- Application state management: User authentication, theme settings
- Form state: Current form values that components need to access
- Configuration settings: App settings that need to be immediately available
ReplaySubject
A ReplaySubject is like a Subject with a buffer. It can store multiple previous values and replay them to new subscribers.
#Key Characteristics of ReplaySubject:
- Configurable buffer: Can specify how many values to store
- Time-based replay: Can set a time window for replaying values
- Historical data: New subscribers receive specified number of previous values
- Flexible replay: Can replay all or a subset of previous emissions
// ReplaySubject with buffer size of 2
let subject = new ReplaySubject<string>(2);
// First subscriber
subject.subscribe((data) => {
console.log("First subscriber got data >>>>> " + data);
});
// Emit multiple values
subject.next("First value");
subject.next("Second value");
subject.next("Third value");
// Second subscriber (added AFTER all emissions)
subject.subscribe((data) => {
console.log("Second subscriber got data >>>>> " + data);
});
subject.next("Fourth value");
// Console result:
// First subscriber got data >>>>> First value
// First subscriber got data >>>>> Second value
// First subscriber got data >>>>> Third value
// Second subscriber got data >>>>> Second value (replayed from buffer)
// Second subscriber got data >>>>> Third value (replayed from buffer)
// First subscriber got data >>>>> Fourth value
// Second subscriber got data >>>>> Fourth value
Key Observation: The second subscriber receives the last 2 values (“Second value” and “Third value”) from the buffer when it subscribes.
Use Cases for ReplaySubject:
- Chat applications: Show last N messages to new users
- Activity logs: Display recent activities to new viewers
- Data caching: Cache recent API responses
- Undo/Redo functionality: Store recent actions for rollback

Conclusion
Understanding the differences between Subject, BehaviorSubject, and ReplaySubject is crucial for effective RxJS usage:
- Subject: Use for simple event broadcasting when you don’t need state
- BehaviorSubject: Use for state management when new subscribers need current value
- ReplaySubject: Use when new subscribers need historical context
The key is matching the Subject type to your specific use case. Start with these examples and experiment to see the differences in behavior!
PS: The Problem: Exposing Subjects Directly
When we expose a Subject directly, external code can call next()
, error()
, and complete()
on it, which can break our application’s data flow control.
Bad Example – Direct Subject Exposure:
class UserService {
// BAD: Exposing the Subject directly
public userState = new BehaviorSubject<User>(null);
login(username: string, password: string) {
// Your login logic here
const user = { id: 1, name: username };
this.userState.next(user); // Service controls the state
}
logout() {
this.userState.next(null); // Service controls the state
}
}
// Usage in a component
const userService = new UserService();
// PROBLEM: Any external code can mess with your state!
userService.userState.next({ id: 999, name: "Hacker" }); // ? This shouldn't be allowed!
userService.userState.complete(); // ? This breaks everything!
userService.userState.error(new Error("Oops")); // ? This crashes your app!
// The service has lost control over its own state!
Good Example – Using asObservable():
class UserService {
// PRIVATE: Only this service can modify the state
private _userState = new BehaviorSubject<User>(null);
// PUBLIC: External code can only subscribe, not modify
public userState$ = this._userState.asObservable();
login(username: string, password: string) {
const user = { id: 1, name: username };
this._userState.next(user); // Only the service can do this
}
logout() {
this._userState.next(null); // Only the service can do this
}
}
// Usage in a component
const userService = new UserService();
// GOOD: External code can only observe
userService.userState$.subscribe(user => {
console.log("User state changed:", user);
});
// PROTECTED: These will cause TypeScript errors
// userService.userState$.next(user); // ? Error: Property 'next' does not exist
// userService.userState$.complete(); // ? Error: Property 'complete' does not exist
// userService.userState$.error(); // ? Error: Property 'error' does not exist
Summary: asObservable()
converts a Subject into a read-only Observable, preventing external code from calling next()
, error()
, or complete()
. This gives us full control over our data flow and prevents bugs caused by external modifications.