
The portfolio is lovely. Smooth animations, a nice dark mode, a case study written in a confident voice. You hire.
Then the first pull request arrives. A new component that does almost the same thing as one that already exists. State fetched in three places. A type that says the value is always there when the API says otherwise. Nothing is broken exactly, and reviewing it takes forty minutes.
The portfolio was not lying. It just measured a different job. Building something alone from scratch and working in a codebase with four other people are separate skills, and only one of them is visible on a personal site.
"Build a to-do app" and its cousins share a shape: start from nothing, deliver something that looks right, alone, with no history and no neighbours.
Every hard thing about real frontend work has been removed. There is no existing code to fit into. No component that already half-does this. No shared state someone else owns. No accessibility requirement anyone will check. No performance problem, because the list has six items.
You end up grading visual polish and speed, which is why portfolio-strong, codebase-weak keeps happening.
The fix is not a harder problem. It is an existing codebase.
Give the candidate a working React and TypeScript app, a few thousand lines, with its own conventions and a couple of honest imperfections. Then:
The setup. A page with a moderately large list. Typing in a filter box makes the whole thing lag. The cause is real: a new object or callback created on every render and passed down, or a context that everything subscribes to.
Strong looks like: they open the profiler before changing code. They find what is re-rendering and why, then make one targeted change. They can say why memoizing everything is a bad idea.
Weak looks like: wrapping components in memo until the symptom goes away, with no measurement. This works often enough to be a trap and it teaches you nothing about the person, unless you notice they never measured.
What it tells you: whether they debug or pattern-match. Same signal as how to assess debugging skill, applied to the browser.
The setup. A custom dropdown or modal built with divs. It looks correct. It cannot be reached by tab, it traps focus in the wrong place, and a screen reader announces nothing useful.
Ask: we had an accessibility audit and this came back. Fix it.
Strong looks like: they actually try it with the keyboard first. They know the difference between adding ARIA attributes and using the right element, and they prefer the second. They check focus management on open and close.
Weak looks like: sprinkling aria-label on things and declaring it done. Or claiming it is fine because a linter did not complain.
What it tells you: whether accessibility is a habit or a phrase from a job ad. For most consumer and public-sector work this is a legal requirement, not a nice-to-have.
The setup. A search box that fires a request per keystroke. Type fast and stale results overwrite fresh ones. Sometimes. It is intermittent, which is the point.
Strong looks like: they reproduce it deliberately, by typing fast or by slowing the network in devtools. They name the cause: responses arriving out of order. They fix it properly with cancellation, an abort signal, or by ignoring responses that are no longer current.
Weak looks like: adding a debounce and calling it fixed. Debounce makes it rarer. It does not make it correct, and knowing the difference is the whole test.
What it tells you: whether they understand async, and whether they can tell "I cannot reproduce it any more" from "it is fixed."
The setup. A response typed as User with everything required, but the API sometimes omits a field. Somewhere downstream, code reads it confidently. TypeScript is perfectly happy and it crashes in production.
Ask: this crashed for a customer last week. Why did the types not catch it?
Strong looks like: they understand that a type is a claim, not a check, and that the claim is unverified at the boundary. They fix it at the edge, with validation or a narrower type, rather than adding optional chaining everywhere the error appeared.
Weak looks like: ?. at the crash site. Or any.
What it tells you: whether they use TypeScript for safety or for autocomplete. This one separates people cleanly and almost nobody tests it.
The setup. A ticket asking for a new card view that is eighty percent the same as an existing one.
Ask: implement this.
Strong looks like: they look for what exists first. They then make a real decision and can defend it, whether that is extending the existing component, extracting a shared piece, or deliberately duplicating because the two things will diverge. Deliberate duplication with a reason is a good answer.
Weak looks like: a new component built from scratch with no sign they looked, and no mention of the one already there.
What it tells you: the single best predictor of what their pull requests will feel like to review.
Four rows, in this order:
Notice that none of these rows is about visual output. If you also care about design sense, test it separately and openly, rather than letting it leak into every score.
All five tasks need a real app, running, with real code around them. The re-render task needs the profiler. The race condition needs network throttling. The accessibility one needs a keyboard and a real DOM. None of that exists in a code editor with a run button.
EasyEnv gives the candidate a real Linux machine with the repo cloned, dependencies installed, and the dev server already running, plus a port view so they can use the app in the browser exactly as a user would. Every candidate starts from the same machine, built from the same recipe, so the bug behaves identically for everyone. The session is recorded, terminal and screen, which for frontend work is unusually useful: you can see whether they opened the profiler, whether they tried the keyboard, and whether they ever reproduced the race before fixing it.
Fair warning on cost. Building this repo is a real project, probably a couple of days for the first version, and someone has to review each session because there is no automatic score. If your open role is junior and high volume, a simpler test is a reasonable call. Build this for the roles where a bad pull request costs you months.
Portfolios measure building alone from nothing. Your job is working in code other people wrote. Test that instead: a re-render nobody profiled, a dropdown no keyboard can reach, a race condition, a type that lies, and a component that already nearly exists.
If you run only one, run the last. Whether a candidate looks for what already exists before writing something new tells you exactly what their pull requests will feel like.
Run live coding sessions and take-home challenges in real production environments. Watch sessions back, score consistently, and hire with confidence.
More posts you might like
Cloud interviews run on IAM trivia while the expensive mistakes are all operational. Here are hands-on tasks on drifted state, a plan that will replace a database, and a module hiding a bad default, plus the cost review that reveals seniority fastest.
Most ML interviews test modeling, which is the part that goes well anyway. Here are the skills that decide whether a model reaches production, and a real-environment task for each, including the LLM engineer variant.
Read moreTimers, proctoring, no-docs rules and think-aloud requirements filter for things you never meant to test. Here is what each one actually measures, the replacement that keeps the difficulty, and how to offer accommodations without a negotiation.
Read more