How to Review Empty States and Microcopy in a Product PR
Empty states and microcopy almost never get their own line in a pull request. They arrive buried inside a component file, tucked between a loading spinner and a retry handler, and reviewers scroll past them on the way to the logic that "matters." That habit is expensive. The empty state is often the first screen a new user sees after signing up, and the error string is the last thing a frustrated user reads before closing the tab. Reviewing them well takes maybe fifteen extra minutes per PR and saves a hundred confused support tickets later.
The difficulty is that these strings and states do not announce themselves. A reviewer looking at a diff sees a gray background and a line of placeholder text and has no idea whether "No items yet" is correct, tone-deaf, or actively misleading for this particular screen. The review has to be deliberate. It has to be a checklist run in the head, not a vibe check.
Find Every Empty State the PR Touches
Start by listing what the diff actually changes. A PR that adds a "Saved Searches" feature will introduce at least three empty states: the list before any search is saved, the list after a user deletes their last search, and the state after a search returns zero results. Reviewers who only look at the first one will miss the other two until QA files a bug.
The test here is simple: for every new data surface, ask what renders when the data is absent. If the component has a fallback like "No data" hardcoded, flag it. If the fallback is a raw null check with nothing rendered, flag it louder. Silent empty states are the worst kind because they look like a rendering bug to the user.
A useful habit is to sketch the state matrix on a sticky note before reading the code. One axis is the user's situation: brand new, returning with data, returning after clearing data, offline. The other axis is what the screen shows: loading, empty, partial, full, error. The PR probably only implements a diagonal of that grid. The review catches the missing cells.
Distinguish First-Run from Nothing-Here
Two empty states look identical in code and completely different to a user. The first-run state greets someone who has never used the feature and needs orientation. The nothing-here state explains why a list that used to have items is now bare, and it needs a different tone and a different action.
A first-run empty state should teach. "No saved searches yet. Save a search from any results page to see it here." That is fine. A nothing-here empty state after the user deleted everything should reassure. "All caught up. Saved searches will appear here when you add them." Same screen, different copy, different emotional register.
The failure mode is a single string doing both jobs. "No saved searches" works for neither: too terse for the newcomer, too cold for the person who just cleaned house. When a PR uses one string for both, ask for the branch. The state is knowable at render time; the copy should reflect it.
Check That the Empty State Is Reachable and Honest
Some empty states exist in the code but never render because the parent component returns early or a guard clause swallows the branch. Other empty states render when they should not, flashing on screen for a frame before real data arrives. Both are review findings.
A concrete check: trace the loading sequence by hand. On mount, does the component show the empty state while the fetch is in flight? If yes, the user sees "No saved searches" for 300 milliseconds and then a populated list. That flicker reads as a glitch and undermines trust in the whole page. The fix is a loading skeleton or a delayed empty state, and the review should call for it.
Honesty matters too. An empty state that says "No results match your filters" when the real cause is a failed API call is a lie dressed as an empty state. The error path deserves its own string, its own icon, and its own action. Reviewers who accept the fallback string as a catch-all are approving a support ticket in advance.
Read the Microcopy Out Loud Before Approving
Microcopy has a rhythm problem that reading on screen hides. The string "No data available to display at this time" looks fine in a diff and sounds like a corporate voicemail when read aloud. The reviewer who mutters the string under their breath catches the stiffness in a way that skimming does not.
Three questions work well for any string in the diff. Does it say what happened? Does it say what to do next? Does it sound like a person wrote it? "No saved searches yet. Save one from any results page." passes all three. "No results found." fails the second. "Oops! Looks like there's nothing here!" fails the third for anything resembling a professional tool.
Watch for the apology reflex. Strings that begin with "Sorry" or "Oops" or "Uh oh" appear friendly in isolation and infantilizing in a serious product. A financial dashboard that says "Oops, no transactions yet!" has broken its own register. The reviewer's job is to ask which voice the product established elsewhere and whether the new string honors it.
Audit Buttons, Links, and the Action Verb
Most empty states carry a call to action, and the CTA is where microcopy review earns its keep. "Get started" is a shrug. "Create your first project" tells the user what the button does. "Add a card" tells them what happens next. The verb in the button is the single most important word in the empty state because it is the only instruction.
The verb should match the destination. A button labeled "Explore" that navigates to a settings page is a small betrayal. A button labeled "Add a teammate" that opens an invite modal is honest. Reviewers can verify this by clicking through the preview build, which takes thirty seconds and catches more mismatches than any amount of code reading.
Also check the button count. Two competing primary buttons in an empty state split attention and slow the user down. One primary action, one secondary link at most, is the pattern that holds up across products. A PR that adds three buttons to a first-run screen should get a comment even if each button individually works.
Check Character Limits and Localization Risk
A string that fits nicely in a 240-pixel empty state card will overflow when translated into German. Reviewers who ignore localization hand a bug to the l10n team months later. The check is quick: does the layout have room for the string to grow roughly a third in length, and does the CSS wrap or truncate gracefully?
Strings baked into the component instead of a translation file are a harder flag. "No saved searches yet" hardcoded in a JSX file cannot be translated without a code change. When a PR introduces new user-facing text without touching the i18n layer, the review should ask for the key. It is a boring comment and a necessary one.
The same logic applies to character limits in the empty state itself. A 12-character heading and a 90-character body is a common pattern. If the new copy blows past those limits and the layout has no auto-height, the card will clip on smaller viewports. Testing at 320 pixels wide catches it in a minute.
Look for Inconsistent Capitalization and Punctuation
Microcopy inconsistency is the small crack that makes a product feel unfinished. One empty state ends with a period, the next does not. One uses sentence case, another title case. One says "Sign in," another says "Log in." None of these break the feature. All of them leak.
A reviewer does not need to memorize the style guide. They need to open two or three existing empty states in the product and compare the new string to them. If the punctuation pattern differs, flag it. If the verb for the same action differs, flag it. The fix is a one-character change and the payoff is a product that reads as designed rather than assembled.
This is also where the PR template helps. If the repo has a microcopy checklist, the review has a place to point. If it does not, a reviewer leaving a comment like "new string uses title case; existing empty states use sentence case" creates a record that the next reviewer can cite. Small notes compound.
Test the Empty State on a Slow Connection
Network throttling exposes empty state bugs that no amount of code reading finds. With the connection set to Slow 3G, the loading sequence stretches into something a user actually experiences: a blank frame, then a placeholder, then the empty state, then the real content. Any flicker or premature string shows up in the right order.
This is also where the offline state reveals itself. If the app has no offline handling, the empty state may render as "No saved searches" even though the truth is "the request never left the device." The user reads that as "I have no data," which is the worst possible message when the real problem is connectivity. A reviewer who throttles the network once per PR catches this class of bug cheaply.
The check extends to error surfaces inside the empty state. If the fetch fails, does the empty state offer a retry, or does it sit there looking like a legitimate empty result? A retry button in the error state is the minimum. A retry that actually refetches rather than reloading the page is better, and the review can verify it by killing the network mid-request.
Leave a Comment on the String, Not Just the Code
The most useful microcopy review leaves a trail. A comment on the line of JSX, tied to the string itself, quoting the proposed replacement, is more likely to be fixed than a vague note at the top of the PR. "Suggest: 'No saved searches yet. Save one from any results page.'" is actionable in a way that "empty state could be friendlier" never is.
When the string is fine, the reviewer can say so briefly and move on. When the state logic is wrong, the comment should point at the branch, not the copy, because no string fixes a state that renders at the wrong time. Separating the two failure modes keeps the PR thread readable and the fixes small.
The habit pays off in aggregate. A team that reviews empty states and microcopy on every PR ends up with a product where the first screen a user sees is also the clearest one, and where the last message before a user leaves is one that invites them back. That outcome is built one diff comment at a time, usually by a reviewer who scrolled past the spinner and stopped at the gray box.

