React Marketplace Template That Works Like an App

You want a marketplace front end: people search for something near them, narrow the results, save a few listings and message a seller. The usual starting point, a product grid next to a sidebar of checkboxes, turns into one long column on a phone. This tutorial builds the four screens a react marketplace template needs (home, search with facets, listing cards and a listing detail page) on the Next.js App Router, with the search state in the URL and a phone layout that behaves like an app. We build Wingo UI and the code uses our blocks, so we are not neutral; the URL and API patterns work with any components.
What does a React marketplace template need?
Four screens around one listing shape, plus a shell that holds them together. A rental site, a used gear market and a classifieds app all share them:
Install the blocks you need. Each one pulls in the components and helpers it uses:
The routes live in one route group, so the shell renders once around every page:
The URL scheme is the one the blocks already use: the shell's header search and the home page's hero search both navigate to /search?q=…&loc=…&cat=… when you pass no onSearch, and the category tiles link to /search?cat=<id>. Build your search page around those three parameters and the default navigation works unchanged.
How do you build the marketplace shell?
The shell is a client component that takes your router's link and push. It renders the header search (what and where), the category strip, the footer and, below 768px, the tab bar. The tab bar keeps clear of the home indicator through the safe area inset, and the page content gets matching bottom padding so the last row is never hidden behind it.
Its defaults are a finished demo, and that is the trap. With no props it shows a signed in sample user, 4 saved listings, 2 unread messages, the sample brand "Northwind Market" and category links to /<id> routes you do not have. So read the session in the server layout and hand it to a client view:
The module scope constants are not style. The shell restarts its search state when recentSearches changes identity, and it does that during render, so an inline recentSearches={[]} sets state on every render until React stops it with a "Too many re-renders" error. The shell does not mount a toast region either, and the search page (saved searches) and the detail page (favorite errors, copied links) both raise toasts, so the Toaster goes here once.
Every link inside the blocks, including the stretched link on each listing card, goes through the linkComponent the shell provides, so a tap on a card navigates on the client without a full page load.
How do you build the home page?
Fetch the listings in a server component and hand them to a small client view that owns the callbacks. The block is a client component, and a function such as onFavoriteChange cannot be passed from a server component to a client one.
defaultLocation="" is the prop that is easiest to miss. The New listings grid keeps only the listings whose location, up to the first comma, equals the chosen town's name, and the default town is Cluj-Napoca. Listings in New York or Berlin would leave the grid on "No listings in Cluj-Napoca yet". An empty string means every location.
The heart is optimistic: it fills at once and rolls back when the promise you return rejects. fetch only rejects on a network error, so the helper throws on an error status itself:
$ npx wingo-ui@latest add marketplace-home-pageThe other props in the view replace sample content that would otherwise ship. stats feeds the line under the headline, which defaults to "1,284,310 listings in 3,180 towns and villages". showTrust and showApp hide the trust tiles (cash on delivery, couriers from $9.99, verified sellers) and the app band with its QR code, until your own labels and appUrl say something true about your service. loading turns the tiles, the rail and the grid into skeletons of their real shape while the hero stays usable.
How should a React listing card behave on a phone?
A listing card looks simple until you put it on a phone. The photos should swipe sideways without fighting the page scroll, a tap anywhere should open the listing, the heart must not open the listing, and a grid of 48 cards should not load 240 images. The Listing Card handles those: the title is one link stretched over the card (so long press gives the native link menu), and vertical scrolling always wins over a photo swipe. Each card renders one plain image until the carousel is needed: with a mouse, on the first pointer or focus on the photo; on a touch screen, when the card comes within 240px of the viewport.
Every block in this tutorial passes the same ListingData object around:
When you use the card on your own pages, such as a saved listings screen, three props matter. size="sm" fits two columns on a phone, priority loads the first row eagerly, and maxImages={1} turns the photo carousel off for cards inside a horizontal rail, because a carousel inside a carousel is a gesture nobody can predict.
$ npx wingo-ui@latest add listing-cardHow do marketplace search filters work in React?
Marketplace search filters need two layouts and live counts. A sidebar of checkboxes breaks in two places: on phones, where it has no room, and on the server, where every facet needs a count. The Filter Panel takes a list of FilterDef objects and ships both layouts in the server HTML with CSS classes that show one of them, so a phone never flashes the sidebar before hydration. From 1024px it is a sticky sidebar where each change applies at once. Below that it becomes a Filters button with the active count, and the button opens a full height sheet that keeps a draft. The apply button counts the draft as you change it, so people see "No results, try fewer filters" before they commit. Swiping the sheet down or pressing Escape throws the draft away. We cover why a sheet beats a popover on a phone in our guide to the React bottom sheet for the web.
The search page builds its facets from the category by default. For your own category, pass filters:
With a search function, the apply button's count is one more call: the page runs search(draft, 0) and reads total, debounced by 250ms, with stale answers dropped. So every pause in a draft costs one page 0 request: keep that query fast.
The options carry no count on purpose. When a checkbox, chips or radio option has none, the page fills it from the counts your search returns, so the numbers next to Canon and Sony always match the current results. An option with a count of 0 is disabled. The price filter has no histogram either: the page draws the bars from the histogram your search returns.
How do you keep the search in the URL?
Parse the URL in the server page, then let a client view own the state and write it back with window.history.replaceState. Next.js integrates native pushState and replaceState calls with its router (usePathname and useSearchParams stay in sync), so the address bar updates without a request to the server on every checkbox. searchParams is a promise in Next.js 16, so the page awaits it.
Two details in this code prevent real bugs. The key on SearchView: when someone runs a new search from the shell's header while already on /search, Next.js renders the same page with new searchParams, and without the key React would keep the old useState value. And search is defined outside the component: the page fetches page 0 again whenever the search function changes, so an inline arrow would refetch on every render.
We use replaceState so Back leaves the search page instead of undoing one checkbox at a time. Switch to pushState if your users expect Back to step through their filters.
What does the search function return?
One page of results per call. Page 0 replaces the results and shows skeletons while it is pending; later pages append as the reader nears the end of the list (or presses Show more with loadMode="button"). Return this shape from /api/search:
Compute each facet's counts with every other filter applied, which is what the sample search (searchListings in the block's data file) does. That is the difference between a count that predicts the next click and one that only describes the whole catalog.
$ npx wingo-ui@latest add marketplace-search-pageOne trade-off: with a search function, the first page loads in the browser after hydration, so the server HTML holds skeletons. Without search, the page runs searchListings over items (listings plus a cityId, a facets value per filter id and keywords) during render, and the results are in the HTML. For a small catalog you can fetch every listing on the server and pass items; for a large one, build indexable category pages separately and keep /search as the app screen.
What goes on the listing detail page?
The photos, the price, the attributes, the seller and a way to make contact. The Listing Detail Page takes the listing and the seller as data and the actions as callbacks. The phone number stays masked until someone presses Show phone, which calls your onRevealPhone, so you can log reveals and rate limit them.
Props you leave out fall back to sample data or a pretend request: no sellerListings shows the sample seller's other ads, no onReport reports a fake success, and no onRevealPhone reveals "(503) 555-0142". The callbacks drive the UI: a rejected onMessage keeps the text and shows the error in the dialog, and whatever onRevealPhone resolves to is shown as the number. Because fetch resolves on a 429 or a 500, check the status before you hand the response back, or a rate limit page ends up printed as a phone number:
Below 1024px, stickyBar (on by default) adds a bottom bar with the price, Call and Message once the contact buttons scroll out of view. The page's status prop is separate from listing.status, which is why the view maps one to the other. status="sold" greys the photos, disables contact and moves the similar listings up, so the page still leads somewhere; "reserved" shows a warning banner and keeps contact open. Return the number in international format (+1 503 555 0142): the tel: link adds a Romanian +4 to a number that starts with 0.
What will you need to change for your market?
The place data, and the category table behind the search page's title and breadcrumb. The blocks ship with sample data, and some of it is regional: the place fields, the radius filter and the distances use the Romanian town list in lib/ro-locations, because the kit started as a classifieds kit for that market.
The sample categories (Bikes, Apartments for sale) live in marketplace-search-page-data.ts. An unknown category id falls back to the last entry, "All listings", with its sample related searches (iPhone 14, Sofa), so replace that table with your own categories, or at least pass relatedSearches and override labels.title as in the search view above.
The sample brand also hides in a few strings the snippets above do not touch. Search the installed files for "Northwind" before launch:
For the rest of the phone details, such as safe areas, tap highlights and touch targets, our list of fixes for a web app that feels native applies to every screen here.
Is there a free React marketplace template?
If you need a react marketplace template free of license fees, one open source option is the Sharetribe Web Template (opens in a new tab), licensed Apache-2.0 on GitHub (opens in a new tab). As of October 2026 its docs describe a React app on a vendored copy of Create React App (which they call deprecated), with Redux, CSS Modules and server-side rendering on a Node.js server, so serverless hosts are out. It talks to the Sharetribe APIs, which makes it the better choice when you run your marketplace on Sharetribe and want its backend instead of building your own. If you have your own backend and want App Router pages you own, the blocks in this tutorial are the shorter path.
Where do you go from here?
Posting is the screen we left out: the Post Listing Wizard covers category, details, photos, price, place and a preview with an autosaved draft. The marketplace blocks and the Listing Card are Pro (see pricing); the Button, Card, Dialog and other basics they build on are free. If you are building more screens than a storefront, the Next.js admin dashboard tutorial covers the back office side, and the Build category has the other screen by screen guides.
FAQ
Is there a free React marketplace template?
Yes. The Sharetribe Web Template is open source under the Apache-2.0 license, but it is built to talk to the Sharetribe APIs and runs on a Node.js server. The Wingo UI marketplace blocks (shell, home, search, listing card, filter panel and detail page) are Pro.
What pages does a marketplace front end need?
At minimum a layout shell with search, a home page, a search results page with filters, a listing detail page and a way to post a listing. Saved listings, messages and an account area usually follow, and they reuse the same listing card.
How should marketplace search filters work on mobile?
Open them in a full height sheet from a Filters button that shows how many filters are active. Keep a draft while the sheet is open, count the results on the apply button as the draft changes, and throw the draft away when the sheet is swiped down or closed.
Should marketplace filters be stored in the URL?
Yes. A search that lives in the URL survives a reload, the back button and a shared link, and the server can render the right page on the first request. In Next.js you can update it with window.history.replaceState without a round trip to the server.
Do the Wingo UI marketplace blocks only work with Romanian cities?
No, but the sample place data is the Romanian town list in lib/ro-locations. The shell takes your own cities through its locations prop and the search page takes your own location filter; the home page reads the town list directly, so pass defaultLocation="" and point its import at your own list, since the block installs as source code.
- Marketplace
- Next.js
- Faceted search
- Mobile UI
- React


