React: Wie Geschwister-Komponenten miteinander kommunizieren

React setzt auf den sog. Top-Down-Datenfluss – direkte Kommunikation zwischen Geschwistern ist nicht vorgesehen. In diesem Beitrag zeigen wir, wann das Heben von State sinnvoll ist, wann es an Grenzen stößt und wie sich per Observable auch häufige UI-Änderungen wie Scrollpositionen performant synchronisieren lassen.

Wie sollten wir denn jetzt zwischen Gewschwisterkomponenten kommunizieren? Die kurze Antwort: Gar nicht.

React folgt dem sog. top-down Datenfluss. State lebt in den oberen Komponenten, Daten fließen über Props nach unten, Events bubblen nach oben. Wenn zwei Geschwister-Komponenten miteinander kommunizieren sollen, ist das ein deutliches Zeichen dafür, dass der State an der falschen Stelle sitzt. Die Lösung: State heben (Lifting up State).

Man verschiebt den gemeinsam genutzten State in die nächsthöhere gemeinsame Elternkomponente. Von dort aus bekommen beide Komponenten ihren Teil per Props – und melden Änderungen per Callback zurück.

Das ist konsistent mit Reacts Architektur und in den allermeisten Fällen auch ausreichend.

Parent
 ├── Sibling A (receives data, sends updates)
 └── Sibling B (receives the same data)

Wo dieses Modell an Grenzen stößt

In bestimmten Situationen wird dieses Modell unpraktisch – vor allem bei stark frequentierten UI-Events. Ein klassisches Beispiel: Zwei scrollbare Listen sollen synchron laufen. Wird die Scrollposition in React State gespeichert, löst jeder Scroll ein Re-Render aus. Bei komplexen Komponenten führt das schnell zu Performanceproblemen.

Das ist kein Fehler in React – sondern ein Hinweis darauf, dass dieser State besser außerhalb der React-Logik behandelt werden sollte.

Eventbasierte Workarounds

Für diese Fälle lohnt sich ein Blick auf eventbasierte Kommunikation. Anstatt auf State und Re-Renders zu setzen, können Komponenten direkt über ein geteiltes Objekt kommunizieren – etwa ein eigenes Observable.

Wichtig dabei: Das Observable wird weiterhin innerhalb von React-Komponenten erzeugt und über den normalen Datenfluss verteilt. So bleibt die Datenflussrichtung nachvollziehbar und die Struktur testbar.

// observable.ts

export type Observable<T> = {
  subscribe(observer: (value: T) => void): () => void;
  emit(value: T): void;
};

export function observable<T>(): Observable<T> {
  const observers = new Set<(value: T) => void>();

  return {
    subscribe(observer) {
      observers.add(observer);
      return () => observers.delete(observer);
    },
    emit(value) {
      for (const observer of observers) {
        observer(value);
      }
    },
  };
}

In der Elternkomponente:

import { observable, Observable } from './observable';

function ParentComponent() {
  const scrollChannel = useRef(observable<number>()).current;

  return (
    <>
      <ScrollList scrollSync={scrollChannel} />
      <ScrollList scrollSync={scrollChannel} />
    </>
  );
}

Jede Kindkomponente subscribed dann auf Änderungen und schickt Updates durch die `emit`-Funktion:

type Props = {
  scrollSync: Observable<number>;
};

function ScrollList({ scrollSync }: Props) {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const unsubscribe = scrollSync.subscribe(scrollY => {
      ref.current?.scrollTo({ top: scrollY });
    });

    return unsubscribe;
  }, [scrollSync]);

  return (
    <div
      ref={ref}
      onScroll={e => scrollSync.emit(e.currentTarget.scrollTop)}
      style={{ overflowY: 'scroll', maxHeight: 200 }}
    >
      {/* scrollable content */}
    </div>
  );
}

Scroll-Änderungen werden so außerhalb von React verarbeitet. Keine State-Änderung, keine unnötigen Re-Renders – nur direktes, performantes UI-Verhalten.

Wann dieser Ansatz sinnvoll ist

Eventbasierte Kommunikation sollte nicht die erste Wahl sein – in der Regel ist das Heben von State der richtige Weg. Aber wenn es um flüchtige, häufig aktualisierte Werte geht, etwa Scroll-Positionen oder Maus-Position, ist es sinnvoll, Reacts Lifecycle bewusst zu umgehen.

Geeignete Beispiele:

  • Scroll-Positionen
  • Mausbewegungen / Pointer-Tracking
  • Temporäre Sichtbarkeiten
  • UI-Messwerte wie Breite, Höhe oder Drag-States

Wichtig ist: Daten, die React nicht zum Rendern braucht, müssen auch nicht im React-State leben.


Fazit

Kommunikation zwischen Geschwister-Komponenten ist in React meistens ein Fall für State-Hoisting. Das sorgt für Klarheit und Nachvollziehbarkeit. Wenn Performance jedoch eine Rolle spielt, lohnt sich der Blick über den Tellerrand: Ein lokal erzeugtes, typisiertes Observable erlaubt schnelle, kontrollierte Updates – ganz ohne den Umweg über Reacts Rendering-System.