The Five-Minute Frontend Self-Review Before Opening a PR
Shipping quickly and reviewing carefully are often treated as competing goals. They are not.
The slowest pull requests are rarely slow because the feature was difficult to understand. They are slow because reviewers must repeatedly point out preventable issues: missing empty states, stale effects, unclear naming, accidental logs, duplicated logic, or a button that submits twice.
A short self-review before opening a pull request can remove much of that friction. It does not need to become a second implementation phase. Five focused minutes is often enough.
My approach uses three passes:
- Behavior — does the feature remain correct outside the happy path?
- Structure — is the code easier to understand than it was before?
- Delivery — is the change ready for another person to review?
Why review after the feature works?
While implementing a feature, you carry a large amount of temporary context in your head:
- why a state variable was introduced;
- which API shape you expected;
- why a condition was added;
- which workaround was only meant for debugging;
- what the design is supposed to do.
That context makes imperfect code feel obvious. A reviewer does not have it. Your future self will not have it either.
The purpose of self-review is to create a small distance between writing and evaluating. You temporarily stop asking:
“How can I make this work?
”
and begin asking:
“If I encountered this change for the first time, would its behavior and intent be clear?
”
Start with the diff, not the editor
An editor shows files in their full context. A Git diff shows the decision you are asking the team to accept.
Before opening a pull request, I prefer to review exactly what changed:
git diff git diff --stat
For staged work:
git diff --cached
This view makes several problems easier to notice:
- unrelated edits accidentally included in the change;
- temporary logging and commented-out code;
- copied blocks with small differences;
- renamed values that were not updated consistently;
- formatting churn hiding the meaningful change;
- files that should not be committed.
The diff is also the closest approximation of what the reviewer will see.
Pass one: review behavior
The first pass asks whether the feature behaves correctly when reality is less cooperative than the demo.
Loading
A request may take longer than expected.
Check whether the interface:
- communicates that work is happening;
- prevents actions that should not run twice;
- preserves useful content while refreshing;
- avoids layout shifts that make the page feel unstable.
Loading is not always a full-page spinner. A disabled submit button, a skeleton, or a subtle progress state may communicate intent more effectively.
Error
Every network request can fail. The useful question is not only whether an error is caught, but whether the user can recover.
Check:
- Is the message understandable?
- Can the action be retried?
- Does the previous valid state remain available?
- Is a technical error being exposed directly to the user?
- Could the failure leave local state inconsistent?
Empty
An empty response is often valid data, not an error.
Lists, search results, charts, dashboards, and filtered views all need an intentional empty state. A blank region forces users to guess whether the application is loading, broken, or simply has no data.
Null and undefined
TypeScript reduces uncertainty, but external data still crosses runtime boundaries.
Look for assumptions such as:
const name = response.user.profile.name
Ask what happens if:
- the user is missing;
- the profile has not been created;
- the field is nullable;
- the API returns an older shape;
- cached data is temporarily incomplete.
The answer is not to add optional chaining everywhere. Decide which data is required, validate it at the boundary, and represent optional data deliberately.
Repeated interaction
Try the impatient-user test:
- double-click the primary action;
- press Enter twice;
- change filters during a request;
- navigate away and back;
- submit while a previous mutation is still running.
These interactions reveal duplicate requests, race conditions, stale responses, and buttons that only look disabled.
Pass two: review structure
Once behavior is sound, review whether the implementation communicates its intent.
Question every new state value
Derived data usually does not need its own state.
Instead of synchronizing:
const [fullName, setFullName] = useState("") useEffect(() => { setFullName(`${firstName} ${lastName}`) }, [firstName, lastName])
derive it during rendering:
const fullName = `${firstName} ${lastName}`
Every state value creates another transition the application must keep correct. During review, ask:
- Is this value truly independent?
- Can it be calculated from props, state, or server data?
- Is an effect being used to imitate an event?
- Could two state values disagree?
Review effects as synchronization
An effect should synchronize React with something outside React: the browser, a subscription, a timer, or another external system.
Be suspicious when an effect:
- transforms data for rendering;
- responds to a user action that already has an event handler;
- copies props into state;
- performs several unrelated jobs;
- depends on an unstable object or function.
Effects are powerful, but they hide execution timing. Direct calculations and event handlers are easier to follow when they fit the problem.
Check component responsibility
A component becomes difficult to maintain when it owns too many reasons to change.
Warning signs include:
- data fetching, transformation, and complex presentation in one component;
- several unrelated modals controlled from the same file;
- a large collection of boolean state values;
- repeated conditional blocks for permissions;
- business rules embedded deep inside JSX.
Do not extract components only to reduce line count. Extract when a piece has a clear responsibility, a meaningful name, or an independent reason to change.
Improve names before adding comments
A comment can explain confusing code, but a better name may remove the confusion entirely.
Compare:
const valid = items.filter((x) => x.status === 1)
with:
const activeSubscriptions = subscriptions.filter( (subscription) => subscription.status === "active", )
During self-review, names deserve attention because implementation speed often produces abbreviations that made sense only while writing the code.
Remove noise
Look for:
- unused imports and variables;
- debugging logs;
- commented-out experiments;
- obsolete TODO comments;
- duplicated transformations;
- unnecessary fragments or wrappers;
- conditions that always evaluate the same way.
Linters can automate part of this work, but they cannot decide whether a piece of code still serves the feature.
Pass three: review delivery
The last pass considers the experience of the reviewer and the risk of merging.
Keep the change focused
If a diff contains a feature, a dependency upgrade, formatting changes, and an unrelated refactor, reviewing it becomes harder.
Separate unrelated work when possible. A focused pull request is easier to understand, test, revert, and release.
Verify user-facing details
Small interface details are easy to miss when the main workflow succeeds:
- Can the feature be used with a keyboard?
- Does focus move somewhere sensible after a dialog closes?
- Do buttons have clear accessible names?
- Is important meaning communicated using more than color?
- Does the layout still work with long text?
- Are destructive actions clearly distinguished?
Accessibility checks often expose broader usability problems, so they are valuable even when formal compliance is not the immediate goal.
Explain the change
A good pull request description should make the reviewer productive before reading the code.
Include:
- what changed;
- why the change is needed;
- how it was tested;
- screenshots for visible UI changes;
- known limitations or follow-up work;
- any migration, configuration, or rollout considerations.
Self-reviewing the description often reveals assumptions that should be made explicit in the implementation.
The five-minute checklist
Use this as a final pass before opening a frontend pull request.
Behavior
- [ ] The happy path works with realistic data.
- [ ] Loading, error, and empty states are intentional.
- [ ] Nullable or missing data is handled at the right boundary.
- [ ] Repeated clicks do not create duplicate work.
- [ ] Stale requests cannot overwrite newer state.
- [ ] The user can recover from expected failures.
Structure
- [ ] New state values are truly necessary.
- [ ] Effects synchronize with external systems rather than derive local data.
- [ ] Components have understandable responsibilities.
- [ ] Names explain intent without relying on comments.
- [ ] Repeated logic has been evaluated for extraction.
- [ ] Debugging code, unused imports, and obsolete comments are removed.
Delivery
- [ ] The diff contains only relevant changes.
- [ ] Keyboard, focus, long-content, and responsive behavior were checked.
- [ ] Automated checks pass.
- [ ] The pull request explains what changed and how it was tested.
- [ ] A reviewer can understand the change without private implementation context.
Automate the mechanical checks
Five minutes should be spent on judgment, not formatting.
Automate what tools can decide consistently:
npm run lint npm run typecheck npm test npm run build
Depending on the project, pre-commit or pre-push hooks can also catch:
- formatting differences;
- unused imports;
- invalid commit messages;
- focused tests such as
.only; - accidentally committed secrets;
- generated files that are out of date.
Automation does not replace review. It preserves human attention for behavior, architecture, and product risk.
Know when five minutes is not enough
A short self-review works well for routine feature work, but some changes require deeper review:
- authentication and authorization;
- payments and subscriptions;
- destructive data operations;
- shared state or caching architecture;
- accessibility-critical workflows;
- large dependency or framework upgrades;
- code handling sensitive user data.
The five-minute checklist is a minimum quality gate, not a ceiling.
Final takeaway
Self-review is not about making code perfect before another developer sees it. It is about respecting the reviewer's attention and catching the issues that are easiest to find while the change is still fresh.
The habit is small:
- Finish the feature.
- Step away from the implementation mindset.
- Read the diff as a reviewer.
- Check behavior, structure, and delivery.
- Then open the pull request.
Five focused minutes can prevent multiple rounds of comments, reduce context switching, and make code review about the decisions that actually benefit from another engineer's perspective.
Further reading
This article was inspired by Nextree's discussion of lightweight frontend self-review: