Startseite / Artikel / Drei TypeScript-Muster, die die Architektur von React-Apps verbessern

Drei TypeScript-Muster, die die Architektur von React-Apps verbessern

Erfahren Sie, wie die Repository-, Observer- und Builder-Muster das Typsystem von TypeScript nutzen, um sauberere und wartbarere React- sowie Next.js-Codebasen zu erstellen.

763 Wörter

TypScript schreiben bedeutet nicht, es gut zu nutzen

Viele Entwickler fügen ihren Variablen einfach Typangaben hinzu und halten das für ausreichend. Doch die wahre Stärke von TypeScript liegt woanders – in den strukturellen Mustern, die eine brüchige Codebasis von einer solchen unterscheiden, die im Laufe der Zeit gut wächst.

Nachdem ich Zeit damit verbracht habe, Produktionsanwendungen mit React und Next.js zu entwickeln, gibt es einige Muster, die sich als echte Game-Changers herausgestellt haben. Es handelt sich dabei nicht um abstrakte Übungen aus Lehrbüchern – sondern um Lösungen für Probleme, auf die man tatsächlich stoßen wird.

1. Repository-Muster – Trennen Sie das Datenabrufen von allem anderen

Wenn Ihre Komponenten direkt fetch() aufrufen, bereiten Sie sich darauf vor, im Falle einer Änderung der API einen mühsamen Neuschreibprozess durchführen zu müssen.

Das Repository-Muster verbirgt den Datenzugriff hinter einer sauberen Schnittstelle:

// repositories/userRepository.ts
interface UserRepository {
  getById(id: string): Promise<User>;
  getAll(): Promise<User[]>;
}

export class ApiUserRepository implements UserRepository {
  async getById(id: string): Promise<User> {
    const res = await fetch(`/api/users/${id}`);
    return res.json();
  }

  async getAll(): Promise<User[]> {
    const res = await fetch('/api/users');
    return res.json();
  }
}

Dadurch sind Ihre Komponenten von einer Abstraktion und nicht von einer konkreten Implementierung abhängig. Das bedeutet, Sie können in Ihren Tests ApiUserRepository durch eine Mock-Version ersetzen, ohne irgendeinen UI-Code anzufassen.

2. Observer-Muster – reaktiver Zustand ohne den Redux-Overhead

Redux erledigt die Aufgabe, aber für Zustände, die nicht besonders komplex sind, kann es sich anfühlen, als würde man für eine kleine Aufgabe einen Vorschlaghammer verwenden. Eine leichte, klassenbasierte Observer-Implementierung, die auf einem typisierten Event-Emitter basiert, bietet eine einfachere Alternative:

// utils/eventBus.ts
type EventMap = {
  'user:loggedIn': { userId: string };
  'cart:updated': { itemCount: number };
};

class TypedEventBus {
  private listeners: Partial<{
    [K in keyof EventMap]: ((payload: EventMap[K]) => void)[]
  }> = {};

  on<K extends keyof EventMap>(event: K, cb: (payload: EventMap[K]) => void) {
    (this.listeners[event] ??= []).push(cb);
  }

  emit<K extends keyof EventMap>(event: K, payload: EventMap[K]) {
    this.listeners[event]?.forEach(cb => cb(payload));
  }
}

export const eventBus = new TypedEventBus();

Alles hier ist stark typisiert – es gibt keine any-Typen, die im Verborgenen lauern. Das bedeutet, es besteht keine Unklarheit darüber, welche Form des Payloads ein bestimmter Zuhörer erwarten sollte.

3. Builder-Muster – Ordnung in der Erstellung komplexer Objekte schaffen

Falls Sie schon einmal mit der Erstellung von Filterobjekten, Konfigurationen für API-Anfragen oder Formschemata voller verschachtelter Bedingungen zu kämpfen hatten, bringt das Builder-Muster sofortige Klarheit:

// builders/queryBuilder.ts
class QueryBuilder {
  private params: Record<string, string> = {};

  withPage(page: number) {
    this.params['page'] = String(page);
    return this;
  }

  withLimit(limit: number) {
    this.params['limit'] = String(limit);
    return this;
  }

  withSearch(term: string) {
    if (term.trim()) this.params['q'] = term;
    return this;
  }

  build(): string {
    return new URLSearchParams(this.params).toString();
  }
}

// Usage
const query = new QueryBuilder()
  .withPage(1)
  .withLimit(20)
  .withSearch('typescript')
  .build();
// → "page=1&limit=20&q=typescript"

Der resultierende Code ist leicht verständlich, lässt sich sauber miteinander verknüpfen, und es ist strukturell unmöglich, die Schritte in der falschen Reihenfolge aufzurufen.

Haupterkenntnisse

  • Repository-Muster: Trennt die Datenzugriffsschicht von der UI-Logik und folgt dem Prinzip der Abhängigkeitsumkehr
  • Observer-Muster: Ermöglicht eine leichte, reaktive Kommunikation zwischen den Teilen der Anwendung ohne umfangreichen Boilerplate
  • Builder-Muster: Macht die Erstellung komplexer Objekte sowohl lesbar als auch sicher
  • Sämtliche drei Muster profitieren erheblich von TypeScripts Schnittstellen und Generikas, wodurch sie deutlich sauberer sind als ihre Entsprechungen in reinem JavaScript

„TypeScript geht nicht nur darum, Typen hinzuzufügen – es schafft eine Verbindung zwischen allen Schichten Ihrer Anwendung.“ – Dan Vanderkam, Autor von Effective TypeScript

Was Sie als Nächstes ausprobieren sollten

Wählen Sie ein Muster aus diesem Artikel und wenden Sie es diese Woche auf ein echtes Modul in Ihrer Codebasis an. Arbeiten Sie jeweils Schritt für Schritt voran und denken Sie daran, dass es sich bei diesen Mustern um Werkzeuge und nicht um Dogmen handelt.

Die stärksten Codebasen werden nicht dadurch definiert, wie viele Typen sie enthalten – selbst in einem TypeScript-Projekt nicht. Sie werden durch Absichtlichkeit definiert: Jedes Muster ist dort, weil es seinen Platz verdient hat.

Zusätzliche Lektüre

  • Wenn KI Ihre React-App schreibt, aber die Prinzipien des sauberen Codes ignoriert — Lernen Sie sieben Gewohnheiten des sauberen Codes – DRY, Einzelverantwortung, Schutzklauseln und mehr – die in von KI generiertem React-Code oft verletzt werden, sowie wie man sie behebt.
  • Verständnis von React Lanes: Wie Bitmasken die Aktualisierungspriorität kodieren — Erfahren Sie, wie React mehrere Aktualisierungsprioritäten mithilfe von Bitmasken in eine einzige Ganzzahl kodiert und warum bitweise Operationen einfache boolesche Flags bei der Scheduling ersetzen.