Building the Input/Output UI in React
Romaji to Kana Converter: React & Next.js
Chapter 5 · Building the Input/Output UI in React
Chapter 4 closed with a real, measured verdict: the version of this converter that should
actually ship calls convert() directly in the browser, with the real
/api/convert route kept alive specifically so the two approaches can be compared
side by side. This chapter builds both — a client-side box that's the real recommended version,
and a second box wired to the API route, deliberately built with the same real request-timing
risks a genuine server-calling UI would face in production.
The Client-Side Box: The Version Meant to Ship
Any component using React state or an event handler needs the "use client"
directive at the top of its file — by default, every component in the App Router is a Server
Component, which can't hold interactive state at all:
convert(input) runs directly inside the render, on every single keystroke — no
useEffect, no debounce, nothing asynchronous at all. That's a deliberate,
Chapter-4-backed choice, not an oversight:
convert() call at well under a microsecond. A
real, fast typist producing a keystroke every 50–100 milliseconds is nowhere close to fast
enough to make that cost add up to anything worth optimizing away — debouncing a call this
cheap would add real complexity to solve a problem that, per Chapter 4's own measured evidence,
doesn't exist.
Real, Verified Live-Typing Behavior
Because convert() runs against whatever's currently in the input box, an
incomplete token renders as raw Latin text until the full token is actually typed — verified
directly by simulating a user typing "kyoto" one character at a time:
This isn't a bug — it's the expected, real behavior of a tokenizer with no lookahead into characters the user hasn't typed yet, and it's the exact same real finding the Astro sibling course made against its own version of this converter, now independently re-confirmed here.
The Server-Comparison Box: A Deliberately Real Second Box
A second box calls the real /api/convert route from Chapter 4 instead of
convert() directly. Built naively — firing a fetch() on every
keystroke with no guard at all — it inherits two genuine real risks that the client-side box,
with no network calls at all, never has to worry about.
Risk 1: A Real, Reproduced Out-of-Order Response
A real HTTP request doesn't always finish in the order it was sent — network timing genuinely varies per request. Simulating exactly that with two real, deliberately staggered async calls (an earlier request that happens to be slow, and a later one that happens to be fast) reproduces a real race condition:
fetch(...).then(setResult) handler applies whichever response lands last on the
wire, not whichever request was fired last. Given Chapter 4's own real measured ~14ms round
trip, and a real typist easily producing keystrokes closer together than that, this isn't an
edge case — it's a genuine, likely outcome for the server-comparison box as written so far.
The Fix: A Latest-Request-Wins Guard
A simple incrementing request id, held in a useRef so it survives across renders
without triggering one itself, lets a response check whether it's still the most recent request
before it's allowed to update the displayed result:
Re-running the exact same staggered-timing scenario against the guarded version:
Hands-On Exercises
Simulate real, live typing of "sushi" one character at a time against convert(), exactly as this chapter did for "kyoto." Predict the output at each step first, then verify with real code.
Reproduce this chapter's own race condition with real code, using three staggered requests instead of two (an early slow one, a middle-timed fast one, and a late medium-speed one). Verify the latest-request-wins guard still produces the correct final result.
📄 View solutionExplain, in your own words, why the client-side box needs no debounce and no request-id guard at all, while the server-comparison box genuinely needs both — tracing each requirement back to a specific real finding from Chapter 4.
📄 View solutionChapter 5 Quick Reference
- Client-side box —
"use client",useState,convert()called directly in render, no debounce needed (Chapter 4's own sub-microsecond finding) - Real, verified live-typing behavior — an incomplete token renders as raw Latin text until it fully completes
- Server-comparison box — a genuine, reproduced out-of-order response race condition, fixed with a
useRef-based latest-request-id guard - A real, justified 300ms debounce — a direct, honest consequence of Chapter 4's own measured ~14ms-per-request cost, not an arbitrary UX choice