React-Tutorial: Ein produktionsreifes Feature-Slice entwickeln

#react tutorial
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Gründer & Lead Developer

Experte für Softwareentwicklung und Legacy-Code-Optimierung

React-Tutorial: Ein produktionsreifes Feature-Slice entwickeln

Die meisten React-Tutorials enden bei „läuft auf meinem Rechner." In echten Produkten ist das der Anfang, nicht das Ende. Ein produktionsreifes Feature-Slice ist eine kleine, durchgängige Werteinheit (eine User Journey), die sicher auslieferbar, einfach zu betreiben und schwer zu brechen ist.

Dieses Tutorial zeigt einen pragmatischen Weg, ein Feature-Slice zu entwickeln mit:

  • Klaren Modulgrenzen (damit die Codebasis wartbar bleibt, wenn Teams wachsen)
  • Vorhersehbarem Server-State (Laden, Fehler, Caching, Wiederholungsversuche)
  • Formularvalidierung und Fehlerbehandlung
  • Tests, die den Slice bei Refactorings schützen
  • Kleinen Observability-Hooks (damit Produktionsprobleme diagnostizierbar sind)

Wenn Sie die tiefere architektonische Begründung für feature-orientierte Modularität suchen, lesen Sie Wolf-Techs Leitfaden zur React-Frontend-Architektur für Produktteams.

Was wir entwickeln: „Teammitglied einladen"-Slice

Wir implementieren einen einzelnen Feature-Slice für eine typische B2B-App:

  • Eine Seite, die aktuelle Teammitglieder auflistet
  • Ein Formular zum Einladen eines neuen Mitglieds per E-Mail und Rolle
  • Server-Validierungsfehler korrekt angezeigt
  • Eine vorhersehbare UX für Lade-, Leer- und Fehlerzustände

Dies ist absichtlich klein. Produktionsreife dreht sich nicht darum, wie viele Features man hineinstopft, sondern darum, wie sicher man einen dünnen vertikalen Slice ausliefern und betreiben kann.

Annahmen und Tooling (ein praktischer Standard-Stack)

Sie können dies an Next.js, Remix oder eine reine SPA anpassen. Um das Tutorial framework-neutral zu halten, nehmen wir an:

  • React mit TypeScript
  • React Router (oder ein beliebiger Router mit einer Route-Level-Komponente)
  • TanStack Query für Server-State
  • React Hook Form + Zod für Formulare und Validierung
  • Testing Library (Komponentenverhalten) und Playwright (Happy-Path-E2E)

Referenzen:

Wenn Sie eine opinionierte „Produktions-Toolkit"-Checkliste möchten, hat Wolf-Tech einen eigenen Leitfaden zu React-Tools für produktionsreife UIs.

Die Slice-Grenze definieren (die Regel, die Spaghetti verhindert)

Ein Feature-Slice sollte diese Fragen beantworten können, ohne das gesamte Repo zu scannen:

  • Wo ist der Route-Einstiegspunkt für diese Fähigkeit?
  • Wo sind seine Typen und sein API-Vertrag?
  • Wo ist seine Datenzugriffslogik?
  • Wo sind seine UI-Komponenten?
  • Wo sind seine Tests?

Ein einfaches feature-orientiertes Layout:

src/
  features/
    teamInvites/
      api/
        teamApi.ts
        teamApi.schemas.ts
      hooks/
        useMembers.ts
        useInviteMember.ts
      ui/
        InviteMemberForm.tsx
        MembersList.tsx
        TeamMembersPage.tsx
      __tests__/
        InviteMemberForm.test.tsx
        MembersList.test.tsx

Dieser Slice besitzt seine UI und Verträge. Gemeinsam genutzte Primitiven (Button, Modal, Fetch-Wrapper, Telemetrie-Client) leben woanders, aber Geschäftslogik dringt nicht in „shared" ein.

Ein einfaches Diagramm, das eine „Team Invites"-Feature-Slice-Grenze mit vier Boxen zeigt, die mit UI, Hooks, API und Tests beschriftet sind. Pfeile fließen von UI zu Hooks zu API, mit Tests, die auf UI und API zeigen.

Mit Verträgen beginnen: Typen, die Refactorings überstehen

Auch wenn Sie keinen „contract-first"-Prozess organisationsweit betreiben, zahlt sich das innerhalb eines Slices schnell aus.

Erstellen Sie Zod-Schemas für die Laufzeitvalidierung (und leiten Sie TypeScript-Typen daraus ab).

// src/features/teamInvites/api/teamApi.schemas.ts
import { z } from 'zod'

export const MemberSchema = z.object({
  id: z.string(),
  email: z.string().email(),
  role: z.enum(['admin', 'member']),
  status: z.enum(['active', 'invited']).default('active'),
})

export const MembersResponseSchema = z.object({
  items: z.array(MemberSchema),
})

export type Member = z.infer<typeof MemberSchema>
export type MembersResponse = z.infer<typeof MembersResponseSchema>

export const InviteMemberRequestSchema = z.object({
  email: z.string().email(),
  role: z.enum(['admin', 'member']),
})

export type InviteMemberRequest = z.infer<typeof InviteMemberRequestSchema>

Warum das in der Produktion wichtig ist:

  • Sie erkennen Backend-Überraschungen frühzeitig (fehlende Felder, unerwartete Nullwerte)
  • Sie können UI-Code sicher weiterentwickeln, da die Grenze explizit ist
  • Ihre Tests können gegen dasselbe Schema validieren

Einen minimalen Fetch-Wrapper hinzufügen (mit vorhersehbaren Fehlern)

Produktions-Bugs stammen oft aus inkonsistenter Fehlerbehandlung. Standardisieren Sie ein kleines Verhalten: Jeder API-Aufruf gibt entweder validierte Daten zurück oder wirft einen typisierten Fehler.

// src/shared/http/httpClient.ts
export class HttpError extends Error {
  status: number
  details?: unknown

  constructor(message: string, status: number, details?: unknown) {
    super(message)
    this.status = status
    this.details = details
  }
}

export async function httpJson<T>(input: RequestInfo, init?: RequestInit): Promise<T> {
  const res = await fetch(input, {
    ...init,
    headers: {
      'Content-Type': 'application/json',
      ...(init?.headers ?? {}),
    },
  })

  const text = await res.text()
  const data = text ? JSON.parse(text) : undefined

  if (!res.ok) {
    throw new HttpError('Request failed', res.status, data)
  }

  return data as T
}

Dies ist absichtlich klein. Bauen Sie keine übertriebene Client-Abstraktion, bis Sie einen klaren Bedarf haben.

Das Slice-API-Modul implementieren

// src/features/teamInvites/api/teamApi.ts
import { httpJson } from '../../shared/http/httpClient'
import {
  InviteMemberRequest,
  InviteMemberRequestSchema,
  MembersResponse,
  MembersResponseSchema,
} from './teamApi.schemas'

function parse<T>(schema: { parse: (x: unknown) => T }, value: unknown): T {
  return schema.parse(value)
}

export async function getMembers(): Promise<MembersResponse> {
  const raw = await httpJson<unknown>('/api/team/members')
  return parse(MembersResponseSchema, raw)
}

export async function inviteMember(req: InviteMemberRequest): Promise<void> {
  const safe = InviteMemberRequestSchema.parse(req)
  await httpJson('/api/team/invite', {
    method: 'POST',
    body: JSON.stringify(safe),
  })
}

Zu diesem Zeitpunkt hat der Slice einen expliziten Vertrag und vorhersehbare Fehler.

Server-State-Hooks hinzufügen (Cache, Laden, Wiederholungsversuche)

Server-State ist kein „Komponentenzustand". Behandeln Sie ihn als eigene Kategorie.

// src/features/teamInvites/hooks/useMembers.ts
import { useQuery } from '@tanstack/react-query'
import { getMembers } from '../api/teamApi'

export function useMembers() {
  return useQuery({
    queryKey: ['team', 'members'],
    queryFn: getMembers,
    staleTime: 30_000,
  })
}

Für die Mutation:

// src/features/teamInvites/hooks/useInviteMember.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { inviteMember } from '../api/teamApi'

export function useInviteMember() {
  const qc = useQueryClient()

  return useMutation({
    mutationFn: inviteMember,
    onSuccess: async () => {
      await qc.invalidateQueries({ queryKey: ['team', 'members'] })
    },
  })
}

Das liefert Ihnen:

  • Ein konsistentes Lademodell
  • Ein konsistentes Fehlermodell
  • Einen vorhersehbaren Aktualisierungsmechanismus nach Mutationen

Die UI mit expliziten Zuständen entwickeln (Laden, Leer, Fehler)

Eine produktionsreife UI geht nicht vom „Happy Path" aus. Sie rendert explizite Zustände.

// src/features/teamInvites/ui/MembersList.tsx
import * as React from 'react'
import { useMembers } from '../hooks/useMembers'

export function MembersList() {
  const { data, isLoading, isError, error } = useMembers()

  if (isLoading) {
    return <div aria-busy="true">Teammitglieder werden geladen…</div>
  }

  if (isError) {
    return <div role="alert">Laden der Mitglieder fehlgeschlagen. {(error as Error).message}</div>
  }

  const items = data?.items ?? []

  if (items.length === 0) {
    return <p>Noch keine Teammitglieder.</p>
  }

  return (
    <ul>
      {items.map(m => (
        <li key={m.id}>
          <strong>{m.email}</strong> ({m.role}, {m.status})
        </li>
      ))}
    </ul>
  )
}

Barrierefreiheitshinweise, die in echten Apps relevant sind:

  • aria-busy für Ladebereiche
  • role="alert" für Fehlermeldungen, die vorgelesen werden sollen
  • Vermeiden Sie „Nur-Spinner"-Zustände ohne Text

Das Formular hinzufügen (Validierung + Server-Fehler)

Client-seitige Validierung verhindert grundlegende Eingabeverschwendung, aber Sie benötigen weiterhin Server-seitige Fehler (doppelte Einladung, unzulässige Rolle, Domain-Beschränkung).

// src/features/teamInvites/ui/InviteMemberForm.tsx
import * as React from 'react'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { InviteMemberRequest, InviteMemberRequestSchema } from '../api/teamApi.schemas'
import { useInviteMember } from '../hooks/useInviteMember'
import { HttpError } from '../../shared/http/httpClient'

export function InviteMemberForm() {
  const invite = useInviteMember()

  const form = useForm<InviteMemberRequest>({
    resolver: zodResolver(InviteMemberRequestSchema),
    defaultValues: { email: '', role: 'member' },
    mode: 'onBlur',
  })

  const [serverError, setServerError] = React.useState<string | null>(null)

  async function onSubmit(values: InviteMemberRequest) {
    setServerError(null)

    try {
      await invite.mutateAsync(values)
      form.reset({ email: '', role: 'member' })
    } catch (e) {
      if (e instanceof HttpError && e.status === 409) {
        setServerError('Für diese E-Mail-Adresse existiert bereits eine ausstehende Einladung.')
        return
      }
      setServerError('Einladung fehlgeschlagen. Bitte versuchen Sie es erneut.')
    }
  }

  return (
    <form onSubmit={form.handleSubmit(onSubmit)} aria-describedby="invite-help">
      <p id="invite-help">Laden Sie ein Teammitglied per E-Mail ein.</p>

      {serverError ? <div role="alert">{serverError}</div> : null}

      <label>
        E-Mail
        <input type="email" {...form.register('email')} autoComplete="email" />
      </label>
      {form.formState.errors.email ? (
        <div role="alert">{form.formState.errors.email.message}</div>
      ) : null}

      <label>
        Rolle
        <select {...form.register('role')}>
          <option value="member">Mitglied</option>
          <option value="admin">Admin</option>
        </select>
      </label>

      <button type="submit" disabled={invite.isPending}>
        {invite.isPending ? 'Einladung wird gesendet…' : 'Einladung senden'}
      </button>

      {invite.isSuccess ? <p>Einladung gesendet.</p> : null}
    </form>
  )
}

Dies ist das gewünschte Muster:

  • An der Grenze validieren (Zod-Schema)
  • Client-Feldfehler und Server-Fehler unterschiedlich anzeigen
  • Immer einen „unbekannter Fehler"-Fallback haben

Die Route-Level-Seite zusammenstellen (der Slice-Einstiegspunkt)

// src/features/teamInvites/ui/TeamMembersPage.tsx
import * as React from 'react'
import { MembersList } from './MembersList'
import { InviteMemberForm } from './InviteMemberForm'

export function TeamMembersPage() {
  return (
    <main>
      <h1>Teammitglieder</h1>
      <InviteMemberForm />
      <h2>Aktuelle Mitglieder</h2>
      <MembersList />
    </main>
  )
}

Verbinden Sie es mit Ihrem Router als normale Route-Komponente.

Produktionshinweis: Wenn Sie React Router, Next.js oder Remix verwenden, bevorzugen Sie Route-Level-Boundaries für Fehler. Ein einzelnes defektes Widget sollte nicht die gesamte App ausblenden.

Minimale Telemetrie hinzufügen (damit der Slice betreibbar ist)

Produktionsreife beinhaltet: „Können wir verstehen, was fehlschlägt?" Sie benötigen für dieses Tutorial keine vollständige Observability-Plattform, aber Sie sollten dafür designen.

Ein leichtgewichtiges Muster:

  • Ein strukturiertes Ereignis loggen, wenn eine Mutation fehlschlägt
  • Status-Codes erfassen
  • Eine Korrelations-ID einbeziehen, wenn Ihr Backend dies unterstützt
// src/shared/telemetry/telemetry.ts
export function track(event: string, props?: Record<string, unknown>) {
  // Dies mit Ihrem echten Tool verbinden (Sentry, Datadog, OpenTelemetry-Exporter, etc.)
  // Diese Schnittstelle stabil halten.
  console.info('telemetry', event, props ?? {})
}

Im catch-Block des Formulars verwenden:

import { track } from '../../shared/telemetry/telemetry'

// ... im catch-Block
track('team_invite_failed', {
  status: e instanceof HttpError ? e.status : 'unknown',
})

Es geht nicht ums Console-Logging. Es geht darum, eine stabile Schnittstelle zu behalten, damit Sie echtes Tooling einbinden können, ohne Slice-Code neu zu schreiben.

Tests, die den Slice schützen (ohne Implementierungsdetails zu testen)

Streben Sie zwei Schichten an:

  • Komponentenverhaltenssests (Testing Library)
  • Einen Happy-Path-E2E-Test (Playwright)

Komponententest: Formular zeigt Server-Fehler an

// src/features/teamInvites/__tests__/InviteMemberForm.test.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { InviteMemberForm } from '../ui/InviteMemberForm'

test('zeigt eine freundliche Meldung wenn der Server Konflikt zurückgibt', async () => {
  // Im echten Code: Fetch mit MSW mocken.
  // Hier nehmen wir an, Ihr Test-Setup stubbt /api/team/invite mit 409.

  render(<InviteMemberForm />)

  await userEvent.type(screen.getByLabelText(/e-mail/i), 'a@b.com')
  await userEvent.selectOptions(screen.getByLabelText(/rolle/i), 'member')
  await userEvent.click(screen.getByRole('button', { name: /einladung senden/i }))

  expect(await screen.findByRole('alert')).toHaveTextContent(/ausstehende einladung/i)
})

E2E-Test: Einladung erscheint in der Liste

Ihr E2E-Test sollte beweisen, dass der vertikale Slice End-to-End funktioniert (UI + API + Persistenz). Wenn Ihre Umgebungen komplex sind, führen Sie E2E gegen ein geseedtes Test-Backend aus.

Halten Sie es minimal: eine Journey, eine Assertion, die wirklich zählt.

„Definition of Done" für ein produktionsreifes Feature-Slice

Dies ist die Checkliste, die verhindert, dass „fertig" „gemergt" bedeutet.

BereichSo sieht „fertig" ausWarum es wichtig ist
VerträgeRequest- und Response-Typen sind explizit (und wenn möglich validiert)Verhindert stille Backend-Drift
ZuständeLade-, Leer- und Fehlerzustände werden bewusst gerendertVerhindert defekte UX unter echten Bedingungen
Server-StateDatenabruf und Mutationen verwenden ein konsistentes Muster (Cache + Invalidierung)Macht Verhalten über die App vorhersehbar
FormulareClient-Validierung + Server-FehlerzuordnungReduziert Support-Aufwand und Nutzer-Frustration
BarrierefreiheitLabels, Alerts, Fokusverhalten sind bewusst gestaltetVerhindert „nur per Maus nutzbar"-Regressionen
TestsMindestens ein Verhaltenstest + eine E2E-JourneySchützt den Slice bei Refactorings
Telemetrie-SchnittstelleFehler können mit strukturierten Ereignissen nachverfolgt werdenMacht Produktionsprobleme diagnostizierbar
Rollout-SicherheitFeature kann bei Bedarf deaktiviert werden (Flag)Macht das Ausliefern umkehrbar

Die letzte Zeile ist wichtig. Viele Teams behandeln Feature-Flags als „Extra". In der Praxis sind Flags ein Risikokontrollinstrument für moderne Lieferung.

Häufige Fallstricke (und wie man sie vermeidet)

Fallstrick: shared-Ordner wird zur Müllhalde

Wenn Sie Slice-spezifische Hilfsfunktionen zu früh in shared verschieben, reproduzieren Sie Kopplung durch „Utilities". Halten Sie shared-Code langweilig und generisch.

Eine schnelle Regel: Wenn es „member", „invoice", „checkout" oder andere Geschäftsbegriffe erwähnt, gehört es wahrscheinlich in einen Feature-Slice.

Fallstrick: Drei Arten von Zustand vermischen

Wenn Sie Server-State, Formular-State und UI-State an einem Ort speichern, werden Änderungen riskant.

  • Server-State: TanStack Query (Cache, Wiederholungen, Invalidierung)
  • Formular-State: React Hook Form
  • UI-State: lokaler Komponentenzustand (Dropdown offen, Modal offen)

Fallstrick: Interna testen

Vermeiden Sie Tests, die interne Hook-Aufrufe oder State-Variablen überprüfen. Bevorzugen Sie das Testen von:

  • Was der Nutzer sieht
  • Was der Nutzer tun kann
  • Was passiert, wenn das Netzwerk ausfällt

Häufig gestellte Fragen

Was ist ein „Feature-Slice" in React? Ein Feature-Slice ist ein selbstständiges Modul, das eine nutzerseitige Fähigkeit End-to-End besitzt: Route-Einstiegspunkt, UI, Datenzugriff, Typen/Verträge und Tests.

Brauche ich TanStack Query für eine produktionsreife React-App? Nicht zwingend, aber Sie brauchen einen konsistenten Server-State-Ansatz. TanStack Query ist ein starker Standard, da es Caching, Laden, Wiederholungsversuche und Invalidierung vereinheitlicht.

Wie groß sollte ein Feature-Slice sein? Klein genug, dass ein Team ihn besitzen und sicher ausliefern kann. Wenn der Slice mehrere unzusammenhängende Journeys enthält, ist er wahrscheinlich zu groß.

Sollte ich das als Next.js-Feature statt als SPA-Route entwickeln? Wenn Sie SEO, Server-Rendering oder Route-Level-Sicherheitsgrenzen benötigen, kann Next.js der bessere Standard sein. Wenn die App größtenteils authentifiziert und hochgradig interaktiv ist, kann eine SPA noch eine starke Wahl sein. Wolf-Techs Next.js-vs.-React-Entscheidungsleitfaden deckt die Trade-offs ab.

Was ist das Mindesttesting für einen „produktionsreifen" Slice? Mindestens: ein Komponentenverhaltenstest, der einen wichtigen Fehlermodus abdeckt, plus ein End-to-End-Happy-Path-Test für die Journey.

Hilfe bei der Standardisierung produktionsreifer Slices in Ihrer React-Codebasis?

Wenn Ihre React-App wächst und die Feature-Arbeit sich durch inkonsistente Muster, fragile State-Behandlung oder schwer debuggbare Produktionsprobleme verlangsamt, kann Wolf-Tech Ihnen helfen, Slice-Grenzen zu definieren, sichere Standards einzuführen (Tests, Lieferung, Observability) und schrittweise zu modernisieren.

Entdecken Sie Wolf-Tech unter wolf-tech.io oder kontaktieren Sie uns direkt unter hello@wolf-tech.io. Wir unterstützen Full-Stack-Lieferung, Code-Qualitätsberatung und Legacy-Modernisierung ohne Big-Bang-Rewrites.