Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Reactive State with BehaviorSubject

Hello! Welcome to the first lesson in our module on Advanced State Management with RxJS.

In the previous modules, we established the foundations of Observables, operators, and Subjects. Now, we'll start applying that knowledge to solve a central challenge in front-end development: managing application state.

Today's lesson focuses on a powerful and common pattern: building a reactive store for centralized application state using a BehaviorSubject. We will explore why BehaviorSubject is uniquely suited for this task and construct a simple, encapsulated state service that can act as a single source of truth for a React application.

This lesson builds directly on your understanding of Subjects from Module 3, particularly their dual nature as both an Observer and an Observable, and the specific characteristics of BehaviorSubject.

1. Why BehaviorSubject is the Right Tool for State

Before we build, let's solidify why BehaviorSubject is the preferred choice for managing state over a plain Subject or other RxJS primitives. A state container has three primary requirements that BehaviorSubject meets perfectly:

  1. It must have a current value at all times. An application's state cannot be in an "empty" or indeterminate condition. BehaviorSubject enforces this by requiring an initial value in its constructor.
  2. New consumers must receive the current value immediately. When a component mounts and subscribes to the state, it needs the most recent value right away to render correctly. BehaviorSubject provides this by emitting its last-held value to any new subscriber.
  3. There must be a way to read the current value on demand. While most interactions with the state will be reactive (subscribing to changes), sometimes you need to synchronously get the current value (e.g., in an event handler to compute the next state). BehaviorSubject provides this with the .getValue() method.

To see these characteristics in action, let's watch a video that provides a clear, practical comparison between Subject and BehaviorSubject.

Behaviorsubject vs Subject rxjs - What to Use?

This video from Monsterlessons Academy provides an excellent demonstration of why BehaviorSubject is superior for state management. Pay close attention to the concepts of initial value, how new subscriptions behave, and the utility of the getValue() method.

Please watch the first 6 minutes and 34 seconds of the video. The presenter will walk through creating, updating, and reading from a BehaviorSubject, and then contrast it directly with a plain Subject.

2. Designing a Reactive Store Service

Now that we've confirmed the "why," let's focus on the "how." A common and robust pattern is to create a dedicated service or module that encapsulates the state logic. This prevents components or other parts of your application from directly manipulating the state, enforcing a more predictable, unidirectional data flow.

The core principles of this pattern are:

  • Encapsulation: The BehaviorSubject instance that holds the state is kept private within the service.
  • Exposure: The service exposes the state as an Observable, not the Subject itself. This is a critical detail. By using the .asObservable() method, we provide a read-only stream to the outside world, preventing consumers from calling .next() on our state subject.
  • Controlled Mutations: The service exposes public functions (often called "actions") that are the only way to update the state. These functions contain the logic for creating the next state and calling .next() on the private subject.

Let's look at a concise article that illustrates this architectural pattern.

Reactive Programming in React with RxJS: Advanced ...

This article from Medium, titled 'Reactive Programming in React with RxJS: Advanced ...', contains a perfect, minimal example of this state service pattern. We will focus only on the section about state management.

Please read the section titled '3. Managing State with RxJS'. Focus on the stateService.js code block. Notice how theme$ is a private BehaviorSubject, but getTheme() exposes it as an observable via asObservable(). The setTheme() function is the dedicated mutator.

3. Building a User Store: A Practical Example

Let's synthesize these concepts and build a simple store to manage a user's profile and authentication status. We'll create a userService.ts file.

Step 1: Define the State Interface

First, we define the shape of our state. Using TypeScript gives us type safety and makes the code self-documenting.

// userService.ts

interface UserState {
  isLoggedIn: boolean;
  username: string | null;
  theme: 'light' | 'dark';
}

Step 2: Create the Initial State and Private Subject

We define the initial state and create our private BehaviorSubject instance. The $ suffix is a common convention for variables that hold an Observable or Subject.

// userService.ts
import { BehaviorSubject } from 'rxjs';

// ... (interface UserState)

const initialState: UserState = {
  isLoggedIn: false,
  username: null,
  theme: 'light',
};

// Private BehaviorSubject holding the state
const state$ = new BehaviorSubject<UserState>(initialState);

Step 3: Expose the Public Observable and Selectors

We expose the entire state object as a read-only observable. It's also a best practice to create "selectors"—observables for specific pieces of the state. This prevents components from depending on the entire state object if they only need a small part of it. We use the map and distinctUntilChanged operators for this.

// userService.ts
import { BehaviorSubject } from 'rxjs';
import { map, distinctUntilChanged } from 'rxjs/operators';

// ... (initialState and state$ definition)

// Public observable for the entire state object
export const userState$ = state$.asObservable();

// Selector for isLoggedIn status
export const isLoggedIn$ = userState$.pipe(
  map(state => state.isLoggedIn),
  distinctUntilChanged() // Only emit when the value actually changes
);

// Selector for the theme
export const theme$ = userState$.pipe(
  map(state => state.theme),
  distinctUntilChanged()
);

Step 4: Create Action Functions for State Mutation

Finally, we create the functions that will modify our state. Notice how they use state$.getValue() to access the current state and then call state$.next() with a new object. This immutability is crucial for predictable state updates and works well with React's rendering model.

// userService.ts

// ... (imports and previous definitions)

export function login(username: string) {
  state$.next({
    ...state$.getValue(), // Get current state
    isLoggedIn: true,
    username: username,
  });
}

export function logout() {
  state$.next({
    ...state$.getValue(),
    isLoggedIn: false,
    username: null,
  });
}

export function toggleTheme() {
  const currentTheme = state$.getValue().theme;
  state$.next({
    ...state$.getValue(),
    theme: currentTheme === 'light' ? 'dark' : 'light',
  });
}

This userService.ts file is now a complete, self-contained, reactive store. Any part of your application can import the selectors (isLoggedIn$, theme$) to reactively listen for changes, or import the action functions (login, logout) to trigger updates.

Conclusion

In this lesson, we've established a foundational pattern for advanced state management with RxJS.

Key Takeaways:

  • BehaviorSubject is the ideal tool for reactive state management because it guarantees an initial value, provides the latest value to new subscribers, and allows for synchronous access with .getValue().
  • A robust state store encapsulates its BehaviorSubject and exposes a read-only observable using .asObservable().
  • State mutations should be handled by dedicated "action" functions that ensure immutability by creating a new state object for each update.
  • Creating fine-grained "selector" observables with operators like map and distinctUntilChanged is an efficient way to expose specific slices of state.

In our next lesson, we will build upon this foundation. Now that we have a centralized source of truth, we will explore how to create derived state by combining our store's observables with other data streams using powerful operators like combineLatest. This will allow us to compute new information reactively whenever our underlying state changes.

Can't find a good explanation? Sign up and we'll make it for you

Sign up