Trade-offs I want you to say out loud in a frontend screen

Silent forks fail screens. Here are the state, async, empty, and a11y trade-offs I want said out loud.

Most frontend screens are not failed by missing a library. They are failed by silent forks — state, errors, a11y, and scope — that never get said out loud. Here are the trade-offs I want to hear.

I am Harry Ashton. I built CodePrepped so engineers stop guessing what hiring managers actually listen for. This sits next to what I mark in a 45-minute screen, questions I hope you ask, and the first ten minutes of a pair. Same bar. Louder decisions.

Photo: fauxels / Pexels.

Say the fork before you polish the wrong branch

I hire people who make the trade-off audible. “I will keep this in local state for the first slice; we can lift it if a sibling needs it” is a decision. Shipping a Context provider “just in case” with no consumer is not senior — it is fear of being asked later.

If you never name the fork, I assume you did not see it. If you name five forks and ship none, I assume you cannot prioritise. One clear trade-off, then code, beats a design-review monologue.

State: local first, lift when a sibling proves it

Default to the smallest state that makes the UI honest. Local component state is fine until two places must stay in sync. URL state is right when the user should share or refresh into the same view. Global stores are a last resort in a 45-minute screen, not a badge.

What I want to hear: which user action would break if this stayed local. What I do not want: Redux in a todo demo because a blog said seniors use it.

Async: optimistic, pessimistic, or blocked — pick one

Every toggle, submit, and fetch has a timing story. Optimistic UI feels fast and lies until the server confirms. Pessimistic UI feels slow and stays honest. A blocked button with a label is often the clearest third option.

Say which one you chose and what the user sees on failure. “I will disable the button, show a short pending label, and restore the previous value if the request fails” is enough. Silence plus a spinner that never ends is a soft no.

Empty, loading, and error are part of the feature

A happy-path list is not a feature. I look for empty, loading, and error before I care about micro-interactions. Empty should say what to do next. Loading should not look like a crash. Error should be recoverable without a refresh ritual.

Trade-off to say out loud: skeleton versus spinner versus keeping the last good data. Pick one that matches the risk. Stale data with a quiet refresh can be right for a dashboard; it is wrong for a payment confirmation.

Accessibility is a scope choice, not a speech

I do not need a WCAG lecture. I need you to bind a11y to the UI you are building. Keyboard reachability for the primary action. Focus after opening a dialog. Labels that a screen reader can use. Contrast you did not accidentally break with a grey-on-grey hover.

Trade-off: “Keyboard and focus are in scope for this cut; decorative motion is not.” That is senior. “We should be fully AA compliant” with no concrete control is theatre.

Components: extract after the second copy, not before

Premature abstraction burns the clock. Duplicating a button twice is fine. Duplicating a form three times without extracting is a smell. I listen for when you notice the repetition and whether you extract the right seam — props for real variation, not a god component with twelve booleans.

Say: “Two identical rows — I will extract a Row once the third appears or the props diverge.” Then keep shipping. Refactors that freeze the demo for fifteen minutes rarely help.

Types: strict where the boundary is sharp

TypeScript is not a personality. Use it where bad data would ship a lie — API responses, form values, discriminated unions for UI states. Avoid painting every temporary variable with ceremony while the empty state is still missing.

Trade-off I like: “I will type the server payload and the view-model; the throwaway mapper can stay narrow.” Trade-off I dislike: fighting the type checker for ten minutes instead of rendering an error branch.

Scope: one vertical slice beats half of everything

If time is tight, ship one path end to end: input, validation, success, failure. A beautiful header with no submit handler is a portfolio piece, not a screen. Tell me what you are cutting and why the user still gets a complete loop.

This is the same instinct I want in a take-home README — see what I actually open. In a live pair, your voice is that README.

What a pass sounds like

A pass is short and binding:

  • you name the highest-risk fork early,
  • you choose a default and stick to it until evidence changes,
  • you make empty, loading, and error visible before polish,
  • you can repeat your plan in one sentence I could hand to another engineer.

I should never have to guess whether you saw the trade-off. If I have to fish for it in the follow-up, you left signal on the table.

What a soft no sounds like

Common soft nos around trade-offs:

  • shipping architecture with no user-visible loop,
  • never mentioning failure modes until I ask,
  • changing approach every five minutes with no reason,
  • hiding behind “it depends” without picking a default,
  • perfect vocabulary in the wrap-up after a silent, brittle build.

Curiosity without a decision is still a miss. A decision without a user in mind often ships the wrong thing.

How to practise saying trade-offs out loud

Do not memorise a speech. Practise a loop on a tiny prompt:

  1. Read the brief once. Write three forks that would change the first slice.
  2. Pick the one fork that is highest risk for the user.
  3. Say the trade-off in one sentence, then build the stricter option.
  4. After ten minutes, note whether naming it saved a rewrite.

CodePrepped’s daily loop and Code Arena bouts are built for that rhythm: short reps, visible states, then a review pass. Silent LeetCode will not train the muscle of narrating product risk. Frontend hiring still listens for it.

Should I narrate every line?

No. Narrate decisions, not keystrokes. “Binding the input to state, then validating on blur” is enough. Reading the alphabet of your code aloud wastes the pair.

What if I change my mind mid-screen?

Say why. “The list grew a filter — URL state is worth it now.” Changing without a reason looks restless. Changing with a user reason looks senior.

Is it bad to ask before choosing?

Ask when the brief is ambiguous. Choose when the risk is yours to own. “Is pagination in scope?” is a good question. “What state library do you want?” is often a stall when local state would do.

How does this show up after I finish?

In the follow-up I will poke the forks you skipped — see what I ask after you finish. If you already said them while building, the follow-up becomes a confirmation, not a rescue.