Create your own
Lesson illustration

Combining States with `combineLatest`

Hello! Welcome back to our course on advanced RxJS.

In our last lesson, we built a foundational reactive store using a BehaviorSubject. We learned how to encapsulate state, expose it as a read-only observable, and create selectors for specific state slices. This gave us a centralized "source of truth."

Today, we'll build on that foundation to explore one of the most powerful concepts in reactive state management: derived state. We will learn how to implement derived state by combining multiple state sources using the combineLatest operator. This allows you to compute new information that automatically updates whenever its underlying dependencies change, leading to cleaner, more declarative, and less error-prone code.

1. Understanding Derived State

Derived state is simply state that is calculated or "derived" from other pieces of state. It's not a new, independent piece of information but rather a transformation or combination of existing state.

Consider a simple example:

  • Source State: firstName$ and lastName$
  • Derived State: fullName$

The fullName$ is derived by combining the latest values from firstName$ and lastName$. Whenever either the first or last name changes, the full name is automatically recalculated. You don't need to write imperative code like onFirstNameChange(updateFullName) and onLastNameChange(updateFullName). You simply declare the relationship, and the reactive system handles the updates.

In RxJS, the primary tool for creating derived state is the combineLatest creation function. It takes an array of observables as input and produces a new observable. This new observable emits an array containing the latest value from each of the input observables, and it does so whenever any of the input observables emit a new value.

A key behavior to remember is that combineLatest will not emit its first value until all of its input observables have emitted at least one value.

2. Creating a View Model with combineLatest

A very common and practical use of derived state is creating a "View Model." A View Model is a single object, exposed as an observable, that contains all the data a specific component or view needs to render. This simplifies data consumption in the UI layer, as the component only needs to subscribe to one stream.

Let's watch a segment of a video that demonstrates this pattern effectively.

I only ever use *these* RxJS operators to code reactively

This video from Joshua Morony's channel clearly explains how combineLatest is used to create a view model for a template. Pay attention to how he combines multiple, separate streams into a single vm$ stream that the UI can easily consume.

Please watch the section from 15:27 to 17:29. The speaker demonstrates creating a vm$ (view model) stream by combining four different state sources.

As you saw in the video, the pattern is to pass an array of source observables to combineLatest and then use the map operator to transform the resulting array of values into a nicely structured object.

Let's apply this to the userService we designed in the last lesson. Imagine our application also has a separate service managing permissions.

// permissions.service.ts
import { BehaviorSubject } from 'rxjs';

// Assume this could be fetched from an API upon login
export const permissions$ = new BehaviorSubject({ isAdmin: false }); 

Now, in a component's logic file or a dedicated view-model file, we can combine our user state with these permissions to create a view model for the application header.

import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { userState$ } from './userService'; // From previous lesson
import { permissions$ } from './permissions.service';

const headerViewModel$ = combineLatest([
  userState$,
  permissions$
]).pipe(
  map(([user, permissions]) => {
    // This mapping function creates the derived state object
    return {
      welcomeMessage: user.isLoggedIn ? `Welcome, ${user.username}` : 'Welcome, Guest',
      currentTheme: user.theme,
      showAdminButton: user.isLoggedIn && permissions.isAdmin
    };
  })
);

// A React component could then subscribe to headerViewModel$ to get all the data it needs.
headerViewModel$.subscribe(vm => {
  console.log('Header View Model:', vm);
});

This headerViewModel$ is our derived state. It will automatically emit a new value whenever userState$ or permissions$ changes.

3. Optimizing Derived State Streams

When you create derived state, especially if the calculation is complex or the stream has many subscribers, it's important to consider performance. We can add a couple of operators to our chain to make it more efficient.

The following article provides a clear explanation and example of this optimization.

Using RxJS and React for Reusable State Management

The article 'Using RxJS and React for Reusable State Management' by Toptal has an excellent section on derived state that introduces two key operators for optimization: shareReplay and distinctUntilChanged.

Please read the section titled 'Derived State With the combineLatest Function'. Pay close attention to the final code block where shareReplay and distinctUntilChanged are added to the pipe.

Let's break down the optimized pattern shown in the article:

const derivedState$ = combineLatest([sourceA$, sourceB$]).pipe(
  map(([valA, valB]) => /* Potentially expensive calculation */),
  shareReplay(1),
  distinctUntilChanged()
);
  1. map(...): This is where you perform the transformation, as we've already seen.
  2. shareReplay(1): This is a powerful multicasting operator. It ensures that the map function (the expensive calculation) is only executed once, even if there are multiple subscribers to derivedState$. It then "replays" the last emitted value (1) to any new subscriber. This prevents redundant work and shares the result among all consumers.
  3. distinctUntilChanged(): This operator ensures that the stream only emits if the newly calculated derived state is actually different from the last one. For example, if sourceA$ emits a new value but the resulting derived state is identical to the previous one, distinctUntilChanged() will prevent an unnecessary emission.

4. Practical Example: A Reactively Filtered List

Let's solidify these concepts with a classic real-world example: a list of items that can be filtered by a search input. The displayed list is derived state, calculated from the full list and the current search term.

We'll have two "source of truth" streams:

  1. products$: A BehaviorSubject holding the complete list of products.
  2. searchTerm$: A BehaviorSubject holding the string from a search input field.

Our goal is to create filteredProducts$, which will reactively update whenever the search term changes.

import { BehaviorSubject, combineLatest } from 'rxjs';
import { map, distinctUntilChanged, shareReplay, startWith } from 'rxjs/operators';

// --- State Sources ---

interface Product {
  id: number;
  name: string;
  category: string;
}

// 1. Source of truth for all products (e.g., from an API call)
const products$ = new BehaviorSubject<Product[]>([
  { id: 1, name: 'RxJS T-Shirt', category: 'Apparel' },
  { id: 2, name: 'React Mug', category: 'Kitchen' },
  { id: 3, name: 'Observable Sticker Pack', category: 'Accessories' },
  { id: 4, name: 'Reactive Programming Book', category: 'Books' },
]);

// 2. Source of truth for the user's search input
const searchTerm$ = new BehaviorSubject<string>('');


// --- Derived State ---

// 3. The filtered list, derived from the products and the search term.
export const filteredProducts$ = combineLatest([
  products$,
  searchTerm$
]).pipe(
  map(([products, term]) => {
    console.log('Calculating filtered products...'); // To see when it runs
    if (!term) {
      return products;
    }
    return products.filter(product => 
      product.name.toLowerCase().includes(term.toLowerCase())
    );
  }),
  shareReplay(1) // Cache and share the result
);

// --- Actions ---

// This function would be called by the 'onChange' event of a search input
export function setSearchTerm(term: string) {
  searchTerm$.next(term);
}

In this setup, a React component would subscribe to filteredProducts$ to render the list and call setSearchTerm when the user types in a search box. The component's logic is minimal; it's just connecting the view to the reactive streams. All the filtering logic is cleanly encapsulated and declaratively defined. This pattern is extremely common and powerful, as also seen in the exportRequest$ example in the "Using RxJS with React" article (resource 99ec0, part 2), which derives an export action's availability from the number of selected rows.

Conclusion

Today we've added a crucial tool to our state management toolbox. By understanding and using derived state, you can build complex, interconnected user interfaces where data flows logically and automatically.

Key Takeaways:

  • Derived State is state that is computed as a pure function of other state sources.
  • combineLatest is the primary RxJS tool for this. It takes an array of observables and emits an array of their latest values whenever any input stream emits.
  • The View Model pattern (combineLatest + map) is an effective way to prepare all the data a component needs in a single, convenient stream.
  • For real-world applications, always consider optimizing derived state streams with shareReplay(1) (to prevent redundant calculations) and distinctUntilChanged() (to prevent redundant emissions).

In our next lesson, we will apply these concepts to another complex and common scenario: handling complex form validation with inter-field dependencies. We'll see how combineLatest can be used to create streams representing the validity of individual fields and the form as a whole.

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

Sign up