Build1 publisher3 min readPublished
React's Server Components remove three useState calls and the API route that fed them
A dev.to walkthrough rebuilds a product search on React 19 and Next.js 16, and the server-rendered version loses its loading flag, its abort controller and its single-consumer endpoint because it only ever runs once.
The Engineer · Build desk
What happened
- A dev.to walkthrough by jgomezdev rewrites a product search for a fictional coffee company as a React Server Component, with every example assuming React 19 and Next.js 16.
- The client-side version runs about thirty lines and holds three pieces of state, an abort controller, and a DOMException check whose only job is to keep a cancelled request from rendering as an error.
- The stated bill is that the component cannot respond to a click, hold state or re-render, so when the user types a new query something else has to make it run again.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint With the server rendering by default, interactivity becomes something you opt into at named leaves, and the place where state lives is now a design decision made per component instead of a default.
- decision Before deleting hooks, a team has to establish whether its search endpoints have consumers beyond the one component; an endpoint with a mobile client or a partner integration survives the migration that removes the hooks.
- capability A 300 KB Markdown parser can be imported, used, and shipped to the browser as nothing at all, so a dependency previously rejected on weight can be reconsidered when the render happens on the server.
The AbortController in the client listing is the fix for one failure: drop it, type "ethiopia" fast enough, and the results on screen are for "ethiopi", because the earlier and slower request landed last [3]. The DOMException check covers the second-order effect of that fix, since aborting throws, and the listing's own comment says a cancelled request is not a failure the user needs to hear about [4]. The author calls this listing "the correct version, including the parts most people leave out" [13].
The Server Component version is an async function that awaits `searchProducts(query)` and returns the list, with no state, no loading flag, no error flag, no abort controller and no `/api/products` endpoint [6]. `searchProducts` reads the database directly, because the function runs on a server and that is allowed now [7]. Both listings exist to move a string from an input box to the database and back; one of them needs an HTTP endpoint to do it. That endpoint, the article notes, had exactly one consumer in the world [5].
Counting the two listings against each other, four kinds of client machinery come out: three useState calls, one useEffect, one AbortController, one DOMException check [16].
The constraint that pays for the deletion is stated as a single sentence. "A Server Component runs once, on the server, and its output is sent to the browser as data. It is never re-run in response to anything the user does," the author wrote [8]. The restrictions then follow by derivation: no useState, because state exists so a component can render again with a different value, and there is no again [10]; no onClick, because a function cannot be sent over the network to a browser that does not have your server's memory [11].
What the supplied text does not show is the other half. The article says that if the user types a new query, something else has to make it run again, and that most of the article is about that [9]. The English text then breaks off mid-sentence at "Your /api/pr" [17]. So the deletion is documented here and the replacement trigger is not. The Spanish edition of the same walkthrough by the same author carries the same listings and the same rule [14], so this counts as one argument.
For the line count to transfer to your repository, a few things have to hold. Your search endpoint needs no consumer other than the component you are deleting, or the handler stays and only the hooks go [5]. `searchProducts` has to be safe to call on the render path, because the await now happens while the page is being produced [7]. And the user-visible states the SPA version rendered still have to come from somewhere once the flags are gone [2].
Version assumptions matter too. Every example assumes React 19 and Next.js 16, where the author says Server Components and the App Router are both production-stable [1]. No release note is quoted. That is the walkthrough's stated premise for its own code.
What to watch
- Whether the rest of the walkthrough shows the trigger that re-runs a Server Component when the query changes, or leaves it as an assertion.
- Whether React and Next.js release notes support the production-stable claim for Server Components and the App Router in versions 19 and 16.
- Whether anyone attaches measured numbers to this migration, such as bundle size or time to first byte; the walkthrough offers line counts only.