React Marketplace Template, das sich wie eine App anfühlt

Du brauchst ein Frontend für einen Marktplatz: Leute suchen etwas in ihrer Nähe, grenzen die Ergebnisse ein, merken sich ein paar Inserate und schreiben einem Verkäufer. Der übliche Ausgangspunkt, ein Produktraster neben einer Seitenleiste voller Checkboxen, wird auf dem Smartphone zu einer einzigen langen Spalte. In diesem Tutorial baust du die vier Screens, die ein React Marketplace Template braucht (Startseite, Suche mit Filtern, Inseratskarten und eine Detailseite), mit dem Next.js App Router: den Suchzustand in der URL und ein Smartphone-Layout, das sich wie eine App verhält. Wir entwickeln Wingo UI, und der Code nutzt unsere Blöcke, neutral sind wir also nicht. Die Muster für URL und API funktionieren mit beliebigen Komponenten.
Was braucht ein React Marketplace Template?
Vier Screens rund um ein gemeinsames Datenformat für Inserate, dazu eine Shell, die sie zusammenhält. Ein Vermietungsportal, ein Markt für gebrauchtes Equipment und eine Kleinanzeigen-App brauchen sie alle:
Installier die Blöcke, die du brauchst. Jeder bringt die Komponenten und Helfer mit, die er nutzt:
Die Routen liegen in einer Route Group, damit die Shell nur einmal gerendert wird und jede Seite umschließt:
Als URL-Schema nimmst du das, was die Blöcke schon verwenden: Die Suche im Header der Shell und die Suche im Hero der Startseite navigieren beide zu /search?q=…&loc=…&cat=…, wenn du kein onSearch übergibst, und die Kategorie-Kacheln verlinken auf /search?cat=<id>. Bau deine Suchseite um diese drei Parameter herum, dann funktioniert die Standard-Navigation ohne Änderung.
Wie baust du die Shell des Marktplatzes?
Die Shell ist eine Client-Komponente, die den Link und die Push-Funktion deines Routers bekommt. Sie rendert die Suche im Header (Was und Wo), die Kategorieleiste, den Footer und unter 768px die Tab-Leiste. Die Tab-Leiste hält über den Safe-Area-Inset Abstand zum Home-Indikator, und der Seiteninhalt bekommt unten den passenden Abstand, damit die letzte Reihe nie dahinter verschwindet.
Ihre Standardwerte ergeben eine fertige Demo, und genau das ist die Falle. Ohne Props zeigt sie einen angemeldeten Beispielnutzer, 4 gemerkte Inserate, 2 ungelesene Nachrichten, die Beispielmarke „Northwind Market“ und Kategorie-Links auf Routen wie /<id>, die es bei dir nicht gibt. Lies also die Session im Server-Layout und gib sie an eine Client-View weiter:
Die Konstanten auf Modulebene sind keine Stilfrage. Die Shell setzt ihren Suchzustand zurück, sobald sich die Referenz von recentSearches ändert, und das passiert während des Renderns. Ein inline übergebenes recentSearches={[]} setzt also bei jedem Render State, bis React mit dem Fehler „Too many re-renders“ abbricht. Außerdem bringt die Shell keine Toast-Region mit. Die Suchseite (gespeicherte Suchen) und die Detailseite (Fehler beim Merken, kopierte Links) lösen aber beide Toasts aus, deshalb kommt der Toaster einmal hierher.
Jeder Link in den Blöcken, auch der Link, der sich über jede Inseratskarte spannt, läuft über die linkComponent der Shell. Ein Tipp auf eine Karte navigiert also im Client, ohne die Seite komplett neu zu laden.
Wie baust du die Startseite?
Lade die Inserate in einer Server-Komponente und gib sie an eine kleine Client-View weiter, die die Callbacks enthält. Der Block ist eine Client-Komponente, und eine Funktion wie onFavoriteChange lässt sich nicht von einer Server-Komponente an eine Client-Komponente übergeben.
defaultLocation="" ist die Prop, die du am leichtesten übersiehst. Das Raster „New listings“ zeigt nur Inserate, deren location bis zum ersten Komma dem Namen des gewählten Orts entspricht, und der Standardort ist Cluj-Napoca. Bei Inseraten in New York oder Berlin bliebe das Raster bei „No listings in Cluj-Napoca yet“ hängen. Ein leerer String bedeutet: alle Orte.
Das Herz ist optimistisch: Es füllt sich sofort und springt zurück, wenn das Promise, das du zurückgibst, abgelehnt wird. Das Promise von fetch wird nur bei einem Netzwerkfehler abgelehnt, deshalb wirft der Helfer bei einem Fehlerstatus selbst einen Fehler:
$ npx wingo-ui@latest add marketplace-home-pageDie übrigen Props in der View ersetzen Beispielinhalte, die sonst mit ausgeliefert würden. stats füllt die Zeile unter der Überschrift, die standardmäßig „1,284,310 listings in 3,180 towns and villages“ lautet. showTrust und showApp blenden die Vertrauens-Kacheln (Barzahlung bei Lieferung, Kurierversand ab $9.99, verifizierte Verkäufer) und den App-Bereich mit QR-Code aus, bis deine eigenen labels und deine appUrl etwas Zutreffendes über deinen Dienst sagen. loading ersetzt Kacheln, Leiste und Raster durch Skeletons in ihrer echten Form, während der Hero bedienbar bleibt.
Wie sollte sich eine React Listing Card auf dem Smartphone verhalten?
Eine Inseratskarte sieht einfach aus, bis du sie aufs Smartphone bringst. Die Fotos sollen sich seitlich wischen lassen, ohne dem Scrollen der Seite in die Quere zu kommen, ein Tipp irgendwo soll das Inserat öffnen, das Herz darf es nicht öffnen, und ein Raster aus 48 Karten darf nicht 240 Bilder laden. Die Listing Card löst das: Der Titel ist ein Link, der über die ganze Karte gezogen ist (langes Drücken öffnet also das native Link-Menü), und vertikales Scrollen hat immer Vorrang vor dem Wischen durch die Fotos. Jede Karte rendert nur ein einfaches Bild, bis das Karussell gebraucht wird: mit der Maus, sobald der Zeiger zum ersten Mal auf das Foto kommt oder es den Fokus bekommt; auf einem Touchscreen, sobald die Karte bis auf 240px an den Viewport herankommt.
Jeder Block in diesem Tutorial reicht dasselbe ListingData-Objekt herum:
Wenn du die Karte auf eigenen Seiten nutzt, etwa auf einem Screen mit gemerkten Inseraten, kommt es auf drei Props an. size="sm" passt zweispaltig aufs Smartphone, priority lädt die erste Reihe sofort, und maxImages={1} schaltet das Foto-Karussell für Karten in einer horizontalen Leiste ab, denn bei einem Karussell im Karussell kann niemand vorhersagen, was eine Geste auslöst.
$ npx wingo-ui@latest add listing-cardWie funktionieren Suchfilter für Marktplätze in React?
Suchfilter für Marktplätze brauchen zwei Layouts und Live-Trefferzahlen. Eine Seitenleiste mit Checkboxen scheitert an zwei Stellen: auf dem Smartphone, wo sie keinen Platz hat, und auf dem Server, wo jeder Filter eine Trefferzahl braucht. Das Filter Panel nimmt eine Liste von FilterDef-Objekten und liefert beide Layouts im Server-HTML aus; CSS-Klassen blenden jeweils eins davon ein. Auf dem Smartphone blitzt die Seitenleiste vor der Hydration also nie kurz auf. Ab 1024px ist es eine Sticky-Seitenleiste, in der jede Änderung sofort greift. Darunter wird daraus ein Button „Filters“ mit der Zahl der aktiven Filter. Er öffnet ein Sheet in voller Höhe, in dem du an einem Entwurf arbeitest. Der Anwenden-Button zeigt schon beim Ändern, wie viele Treffer der Entwurf hat. So sehen die Leute „No results, try fewer filters“, bevor sie sich festlegen. Wer das Sheet nach unten wischt oder Escape drückt, verwirft den Entwurf. Warum ein Sheet auf dem Smartphone besser ist als ein Popover, erklären wir in unserem Guide zum React Bottom Sheet fürs Web.
Die Suchseite leitet ihre Filter standardmäßig aus der Kategorie ab. Für deine eigene Kategorie übergibst du filters:
Mit einer search-Funktion ist die Zahl auf dem Anwenden-Button nur ein weiterer Aufruf: Die Seite ruft search(draft, 0) auf und liest total aus, mit 250ms Debounce; veraltete Antworten verwirft sie. Jede Pause beim Bearbeiten des Entwurfs kostet also eine Anfrage für Seite 0. Sorg dafür, dass diese Abfrage schnell ist.
Die Optionen haben absichtlich kein count. Fehlt es bei einer Option vom Typ checkbox, chips oder radio, füllt die Seite es aus den counts, die deine Suche zurückgibt. Die Zahlen neben Canon und Sony passen also immer zu den aktuellen Ergebnissen. Eine Option mit 0 Treffern ist deaktiviert. Auch der Preisfilter hat kein histogram: Die Seite zeichnet die Balken aus dem histogram, das deine Suche zurückgibt.
Wie speicherst du die Suche in der URL?
Werte die URL in der Server-Seite aus und lass eine Client-View den State verwalten, die ihn mit window.history.replaceState zurück in die URL schreibt. Next.js bindet native Aufrufe von pushState und replaceState in seinen Router ein (usePathname und useSearchParams bleiben synchron). Die Adressleiste aktualisiert sich also, ohne dass jede Checkbox eine Anfrage an den Server schickt. searchParams ist in Next.js 16 ein Promise, deshalb wartet die Seite per await darauf.
Zwei Details in diesem Code verhindern echte Bugs. Der key auf SearchView: Startet jemand aus dem Header der Shell eine neue Suche, während er schon auf /search ist, rendert Next.js dieselbe Seite mit neuen searchParams, und ohne den Key würde React den alten Wert aus useState behalten. Und search steht außerhalb der Komponente: Die Seite lädt Seite 0 neu, sobald sich die Funktion search ändert. Eine inline definierte Arrow Function würde also bei jedem Render neu laden.
Wir nutzen replaceState, damit der Zurück-Button die Suchseite verlässt, statt jede Checkbox einzeln rückgängig zu machen. Wechsle zu pushState, wenn deine Nutzer erwarten, mit Zurück Schritt für Schritt durch ihre Filter zu gehen.
Was gibt die Suchfunktion zurück?
Pro Aufruf eine Seite mit Ergebnissen. Seite 0 ersetzt die Ergebnisse und zeigt Skeletons, solange sie lädt; spätere Seiten werden angehängt, wenn sich jemand dem Ende der Liste nähert (oder mit loadMode="button" auf Show more drückt). Diese Form muss /api/search zurückgeben:
Berechne die Trefferzahlen jedes Filters so, dass alle anderen Filter schon angewendet sind. Genau das tut die Beispielsuche (searchListings in der Datendatei des Blocks). Das ist der Unterschied zwischen einer Zahl, die den nächsten Klick vorhersagt, und einer, die nur den ganzen Katalog beschreibt.
$ npx wingo-ui@latest add marketplace-search-pageEin Kompromiss bleibt: Mit einer search-Funktion lädt die erste Seite nach der Hydration im Browser, das Server-HTML enthält also Skeletons. Ohne search wendet die Seite beim Rendern searchListings auf items an (Inserate plus eine cityId, ein facets-Wert pro Filter-ID und keywords), und die Ergebnisse stehen im HTML. Bei einem kleinen Katalog kannst du alle Inserate auf dem Server laden und items übergeben. Bei einem großen baust du indexierbare Kategorieseiten separat und behältst /search als App-Screen.
Was gehört auf die Detailseite eines Inserats?
Die Fotos, der Preis, die Merkmale, der Verkäufer und ein Weg, Kontakt aufzunehmen. Die Listing Detail Page bekommt das Inserat und den Verkäufer als Daten und die Aktionen als Callbacks. Die Telefonnummer bleibt maskiert, bis jemand auf Show phone drückt. Der Klick ruft dein onRevealPhone auf. So kannst du jedes Aufdecken protokollieren und per Rate Limit begrenzen.
Lässt du Props weg, greift die Seite auf Beispieldaten oder eine simulierte Anfrage zurück: Ohne sellerListings erscheinen die anderen Anzeigen des Beispielverkäufers, ohne onReport meldet sie einen simulierten Erfolg, und ohne onRevealPhone erscheint „(503) 555-0142“. Die Callbacks steuern die UI: Wird das Promise von onMessage abgelehnt, bleibt der Text erhalten und der Dialog zeigt den Fehler, und was onRevealPhone liefert, wird als Nummer angezeigt. Weil das Promise von fetch auch bei einem 429 oder einem 500 erfüllt wird, prüf den Status, bevor du die Antwort zurückgibst. Sonst steht am Ende eine Rate-Limit-Seite als Telefonnummer da:
Unter 1024px blendet stickyBar (standardmäßig aktiv) unten eine Leiste mit Preis, Anrufen und Nachricht ein, sobald die Kontakt-Buttons aus dem Bild scrollen. Die Prop status der Seite ist unabhängig von listing.status, deshalb übersetzt die View das eine ins andere. status="sold" graut die Fotos aus, deaktiviert den Kontakt und schiebt die ähnlichen Inserate nach oben, damit die Seite trotzdem weiterführt; "reserved" zeigt ein Warnbanner und lässt den Kontakt offen. Gib die Nummer im internationalen Format zurück (+1 503 555 0142): Der tel:-Link setzt ein rumänisches +4 vor eine Nummer, die mit 0 beginnt.
Was musst du für deinen Markt anpassen?
Die Ortsdaten und die Kategorietabelle hinter dem Titel und der Breadcrumb der Suchseite. Die Blöcke kommen mit Beispieldaten, und ein Teil davon ist regional: Die Ortsfelder, der Umkreisfilter und die Entfernungen nutzen die rumänische Städteliste in lib/ro-locations, weil das Kit ursprünglich für Kleinanzeigen auf diesem Markt entstanden ist.
Die Beispielkategorien (Bikes, Apartments for sale) liegen in marketplace-search-page-data.ts. Eine unbekannte Kategorie-ID fällt auf den letzten Eintrag zurück, „All listings“, mit seinen verwandten Beispielsuchen (iPhone 14, Sofa). Ersetz diese Tabelle also durch deine eigenen Kategorien oder übergib wenigstens relatedSearches und überschreib labels.title wie in der Such-View oben.
Die Beispielmarke versteckt sich außerdem in ein paar Strings, die die Snippets oben nicht anfassen. Durchsuch die installierten Dateien nach „Northwind“, bevor du live gehst:
Für die übrigen Details auf dem Smartphone, etwa Safe Areas, Tap-Highlights und Touch-Ziele, gilt für jeden Screen hier unsere Liste mit Fixes für eine Web-App, die sich nativ anfühlt.
Gibt es ein kostenloses React Marketplace Template?
Wenn du ein React Marketplace Template ohne Lizenzkosten brauchst, ist das Sharetribe Web Template (opens in a new tab) eine Open-Source-Option, lizenziert unter Apache-2.0 auf GitHub (opens in a new tab). Stand Oktober 2026 beschreibt seine Doku eine React-App auf einer mitgelieferten Kopie von Create React App (die sie selbst als veraltet bezeichnet), mit Redux, CSS Modules und Server-Side Rendering auf einem Node.js-Server. Serverless-Hosts fallen also weg. Es arbeitet mit den APIs von Sharetribe und ist damit die bessere Wahl, wenn dein Marktplatz auf Sharetribe läuft und du dessen Backend nutzen willst, statt ein eigenes zu bauen. Hast du ein eigenes Backend und willst Seiten im App Router, die dir gehören, sind die Blöcke aus diesem Tutorial der kürzere Weg.
Wie geht es weiter?
Das Einstellen von Inseraten ist der Screen, den wir ausgelassen haben: Der Post Listing Wizard deckt Kategorie, Details, Fotos, Preis, Ort und eine Vorschau mit automatisch gespeichertem Entwurf ab. Die Marketplace-Blöcke und die Listing Card sind Pro (siehe Preise); Button, Card, Dialog und die anderen Grundbausteine, auf denen sie aufbauen, sind kostenlos. Baust du mehr als nur die Storefront, deckt das Tutorial zum Admin-Dashboard mit Next.js die Backoffice-Seite ab, und die Kategorie Build hat die übrigen Guides, die Screen für Screen vorgehen.
Häufige Fragen
Gibt es ein kostenloses React Marketplace Template?
Ja. Das Sharetribe Web Template ist Open Source unter der Apache-2.0-Lizenz, ist aber für die APIs von Sharetribe gebaut und läuft auf einem Node.js-Server. Die Marketplace-Blöcke von Wingo UI (Shell, Startseite, Suche, Listing Card, Filter Panel und Detailseite) sind Pro.
Welche Seiten braucht das Frontend eines Marktplatzes?
Mindestens eine Layout-Shell mit Suche, eine Startseite, eine Seite mit Suchergebnissen und Filtern, eine Detailseite pro Inserat und einen Weg, ein Inserat einzustellen. Gemerkte Inserate, Nachrichten und ein Kontobereich kommen meist dazu, und sie nutzen dieselbe Inseratskarte.
Wie sollten die Suchfilter eines Marktplatzes auf dem Smartphone funktionieren?
Öffne sie in einem Sheet in voller Höhe über einen Button „Filter“, der zeigt, wie viele Filter aktiv sind. Arbeite mit einem Entwurf, solange das Sheet offen ist, zeig auf dem Anwenden-Button die Trefferzahl, sobald sich der Entwurf ändert, und verwirf ihn, wenn das Sheet nach unten gewischt oder geschlossen wird.
Sollten die Filter eines Marktplatzes in der URL stehen?
Ja. Eine Suche in der URL übersteht ein Neuladen, den Zurück-Button und einen geteilten Link, und der Server kann schon bei der ersten Anfrage die richtige Seite rendern. In Next.js aktualisierst du sie mit window.history.replaceState, ohne Umweg über den Server.
Funktionieren die Marketplace-Blöcke von Wingo UI nur mit rumänischen Städten?
Nein, aber die Beispieldaten für Orte sind die rumänische Städteliste in lib/ro-locations. Die Shell nimmt deine eigenen Städte über die Prop locations, die Suchseite deinen eigenen Ortsfilter; die Startseite liest die Städteliste direkt, also übergib defaultLocation="" und stell ihren Import auf deine eigene Liste um, denn der Block wird als Quellcode installiert.
- Marktplatz
- Next.js
- Facettensuche
- Mobile UI
- React


