Skip to the solution
MediumFrontend9 min read

How do you pass data between sibling components that don't have a direct parent-child relationship?

470 views

The problem

Question presented to candidate: "We have a product list and a cart badge in different parts of the page. Neither is a parent of the other. How do you get data from one to the other in Angular, and how do you choose between the options?"

What a strong answer should cover:

  • Default answer: a shared service holding state as signals, injected into both siblings with inject(); expose read-only signals and methods for changes.
  • Scope matters: providedIn: "root" gives one app-wide instance; providers: [...] on a common ancestor gives one instance per ancestor subtree.
  • Lifting state up to the nearest common parent with inputs, outputs or model() works when the siblings are close.
  • RxJS alternative: a BehaviorSubject in the service (late subscribers get the current value; a plain Subject does not).
  • Other channels: the router (URL and query params) for shareable state, a state library (NgRx, signal store) for large apps.
  • Avoid: global variables, reaching into siblings through ViewChild chains, or event buses with no ownership.

Clarifying questions expected:

  • "Should this state survive navigation or a page refresh?"
  • "Is there one instance of each sibling, or could the pair appear several times on a page?"

Code / implementation expected: Yes β€” a signal-based service shared by two sibling components.

  • components
  • state

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

9 min read Β· 11 sections

Read the code

AngularTwo siblings sharing a signal-based CartStore service
import { Component, Injectable, computed, inject, signal } from "@angular/core";

// THE SHARED STORE: one instance for the whole app (providedIn: "root").
@Injectable({ providedIn: "root" })
export class CartStore {
  private readonly _items = signal<string[]>([]);    // private writable state
  readonly items = this._items.asReadonly();          // public read-only view
  readonly count = computed(() => this._items().length);

  add(item: string) {
    this._items.update(list => [...list, item]);      // the only way in
  }
  clear() {
    this._items.set([]);
  }
}

// SIBLING 1: writes through the store. Knows nothing about the badge.
@Component({
  selector: "product-list",
  template: `
    @for (p of products; track p) {
      <button (click)="cart.add(p)">Add {{ p }}</button>
    }
  `,
})
export class ProductListComponent {
  protected cart = inject(CartStore);
  products = ["Tea", "Coffee"];
}

// SIBLING 2: reads from the store. Knows nothing about the list.
@Component({
  selector: "cart-badge",
  template: `
    <p>Cart ({{ cart.count() }}): {{ cart.items().join(", ") || "empty" }}</p>
    <button (click)="cart.clear()">Clear</button>
  `,
})
export class CartBadgeComponent {
  protected cart = inject(CartStore);
}

// PARENT: just places the siblings. No inputs, no outputs, no wiring.
@Component({
  selector: "app-root",
  imports: [ProductListComponent, CartBadgeComponent],
  template: `
    <product-list />
    <cart-badge />
  `,
})
export class AppComponent {}
AngularScoping a store per panel with component providers
import { Component, Injectable, inject, signal } from "@angular/core";

// NOT providedIn root: each component that lists it in providers gets its own copy.
@Injectable()
export class SelectionStore {
  static created = 0;
  readonly id = ++SelectionStore.created;
  readonly selected = signal<string | null>(null);
}

@Component({
  selector: "color-picker",
  template: `
    @for (c of colors; track c) {
      <button (click)="store.selected.set(c)">{{ c }}</button>
    }
  `,
})
export class ColorPickerComponent {
  protected store = inject(SelectionStore); // resolved from the nearest panel
  colors = ["red", "blue"];
}

@Component({
  selector: "color-preview",
  template: `<p>store #{{ store.id }} selected: {{ store.selected() ?? "none" }}</p>`,
})
export class ColorPreviewComponent {
  protected store = inject(SelectionStore);
}

// The PANEL scopes the store: its two children are siblings that share it.
@Component({
  selector: "color-panel",
  imports: [ColorPickerComponent, ColorPreviewComponent],
  providers: [SelectionStore],
  template: `<color-picker /><color-preview />`,
})
export class ColorPanelComponent {}

@Component({
  selector: "app-root",
  imports: [ColorPanelComponent],
  template: `<color-panel /><color-panel />`,
})
export class AppComponent {}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 55 of 121 in Angular

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Angular questions
Up nextMedium

What are functional HTTP Interceptors in modern Angular, and how do you register them?

Continue