Alexander Claes
← Writing

What Makes a Frontend Feature Hard to Review

A frontend feature can be technically correct and still be hard to review. The real difficulty is often context: business rules, state ownership, shared code and edge cases.

Alexander Claes Alexander Claes 9 min read
Blog Front-end
An isometric illustration of a frontend interface under a magnifying glass, revealing connected state, business rules and warnings.

Some frontend features are difficult to review because the code is technically complicated. Most of the time, however, that is not the real problem.

A feature can be hard to review even when every individual line looks reasonable. The component renders, the API call works, the tests pass and the interface does what the ticket says. But when you open the pull request, it still takes a long time to understand what actually changed and whether the behaviour is correct.

That kind of review difficulty is easy to underestimate. We often talk about code review as if it is mainly about spotting mistakes in code. In practice, reviewing a frontend feature also means reconstructing the problem, understanding the business rule, checking the user flow, comparing the new behaviour with existing patterns and deciding whether the change fits the rest of the application.

On a large application, that context is often harder to review than the code itself.

The diff contains more than one change

The most difficult pull requests are often the ones where several different changes are mixed together. A feature is added, an old component is refactored, some naming is improved, a helper is moved, a few tests are rewritten and a styling issue is fixed along the way.

None of those changes may be bad on their own. The problem is that the reviewer now has to understand which lines are part of the actual feature and which lines are cleanup. That makes it much harder to answer the most important review question: did we change the behaviour correctly?

This becomes especially risky in a business-heavy application. A small refactor in a component that handles purchase requests, approvals or dossier statuses might look harmless, but there may be old conditions in that code for a reason. If the refactor is bundled with a new feature, the reviewer has to verify both the new behaviour and the old behaviour at the same time.

I have learned to be much more careful with this. If a component needs cleanup before a feature can be implemented, that cleanup is often worth doing separately. A small preparatory refactor can make the actual feature easier to review, but only if it really stays preparatory. Once behaviour changes are mixed into it, the boundary disappears again.

The user story leaves room for interpretation

A frontend change usually starts from a user story, which should describe both the expected result and the rules behind it. If a button should only be visible for certain users, or a field should be disabled when a dossier is in a specific status, those conditions should be part of the acceptance criteria.

When essential context is missing or ambiguous, it should be clarified and added to the user story before implementation. This gives both the developer and the reviewer the same source of truth. The implementation and pull request can then make it clear how the code applies those documented rules.

Even when the rule is documented, the code can still be difficult to review if its connection to the user story is not clear. A condition such as this might be perfectly valid:

const canEdit =
  request.status === 'DRAFT' &&
  request.allowedActions.includes('EDIT')

The acceptance criteria should answer the business questions behind this condition. Is draft status enough? Why do we also check allowed actions? Should submitted requests ever be editable? What happens for administrators? Is this matching an existing backend rule, or are we inventing a second rule in the browser? The reviewer can then focus on whether the implementation matches the documented rule instead of having to guess what that rule is.

A good pull request does not need to repeat the entire user story, but it should make the important implementation decisions traceable. If a condition exists because the backend returns allowed actions, say that. If a screen follows the same rule as another flow, link to the relevant story or flow. If a strange fallback exists because older records do not contain a field, explain that somewhere the reviewer will actually see it.

The component has to interpret too much

Frontend features become harder to review when components receive raw data and then have to interpret it directly. At first this feels convenient. The API returns an object, the component renders it and a few computed values handle the display details.

Over time, those display details often become domain decisions. A missing requester becomes “Unknown”. A rejected status becomes a red badge. A submitted date only appears for certain statuses. A button is hidden based on allowed actions. A tooltip explains why an action is unavailable.

When this logic lives directly inside a large component, the reviewer has to inspect the template, computed properties, event handlers and sometimes the API response at the same time. It becomes difficult to see which parts are presentation and which parts are interpretation.

This is one reason I like using view models for more complicated screens. A mapping function gives the reviewer a smaller place to check those decisions:

return {
  reference: response.reference,
  requesterLabel: response.requester
    ? `${response.requester.firstName} ${response.requester.lastName}`
    : 'Unknown',
  status: getRequestStatus(response.status),
  canEdit: response.allowedActions.includes('EDIT'),
}

The component can still be reviewed, but it no longer has to explain the entire API contract. The transformation becomes its own reviewable unit.

State ownership is unclear

Another common reason a frontend feature becomes hard to review is unclear state ownership. A screen might have server data, form input, selected rows, filters, modal state, permissions and temporary error messages. If all of that state is handled in the same place and in the same way, the reviewer has to figure out what owns what.

Server state is especially easy to blur into the rest of the interface. If a component fetches data, stores it locally, modifies it after a mutation and then also uses it to control visible actions, the reviewer has to understand the entire lifecycle. When is the data fresh? What happens after saving? Which other screens need to update? Can another user change this at the same time?

Tools like TanStack Query help because they make some of those questions explicit. The query key identifies the data, the mutation describes the write, and invalidation shows which cached data becomes outdated. That does not remove the need for judgment, but it gives the reviewer a familiar structure to follow.

The same applies to client state. If a modal is open, that probably belongs near the screen. If filters affect the URL, that may belong in the router. If a form has unsaved edits, that should not be casually replaced by a background refetch. Review becomes easier when the code makes those ownership boundaries visible.

The feature changes existing behaviour indirectly

Some frontend changes are hard to review because the most important effect is not in the new code. It is in the behaviour that already existed.

Adding a new prop to a shared component is a simple example. The new feature might need a table row to show an extra action, so the table component gets a new option. That option works for the new screen, but the reviewer also has to check whether the table is used elsewhere and whether the default behaviour still makes sense.

The same thing happens with shared helpers, validation rules, formatting functions and generic components. A change may be motivated by one feature but affect many screens. If the pull request does not make that clear, the reviewer has to discover the blast radius manually.

This is not an argument against shared code. Shared code is useful. But the more places a change can reach, the more the pull request needs to help the reviewer see that reach. Sometimes that means adding tests around the shared helper. Sometimes it means listing the screens that were checked. Sometimes it means deciding that a local solution is better than expanding a shared abstraction too early.

The happy path is obvious, but the edge cases are not

A lot of frontend features are reviewed by following the happy path. Open the screen, perform the action, see the expected result. That is necessary, but it is rarely enough.

The difficult behaviour often sits around the edges. What happens when there are no results? What if the user does not have permission? What if the request fails after the user already changed form values? What if the data refreshes while a dialog is open? What if the backend returns a status the frontend does not recognize yet?

These cases matter more in large applications because the interface is not just displaying static data. It is coordinating with a server, with user permissions, with long-running business processes and sometimes with other users working in the same system.

A feature is much easier to review when those states are visible in the implementation and, where useful, in tests or Storybook stories. The reviewer should not have to imagine every possible empty, loading, disabled and error state from scratch.

The pull request is large because the feature was built too quietly

Sometimes a review is difficult because too many decisions were made before anyone else saw the work. By the time the pull request is opened, the implementation has already settled on a structure, an API shape, a component boundary and a set of assumptions about the business flow.

If one of those assumptions is wrong, review becomes painful. The reviewer is no longer commenting on an idea; they are asking for a change to something that already looks finished. That makes feedback more expensive, both technically and socially.

For larger frontend features, it often helps to make the shape visible earlier. That can be a small draft pull request, a quick discussion about the data model, or a narrow first step that establishes the pattern before the full feature is built. The goal is not to add process for its own sake. The goal is to avoid discovering the fundamental disagreement at the end.

Good reviewability is part of good implementation

It is tempting to think of reviewability as something separate from implementation. First we write the code, then someone reviews it. But the way we write the code determines how possible that review is.

A reviewable frontend feature has clear boundaries. It separates behaviour changes from cleanup where possible. It makes business rules visible. It gives data transformation a place to live. It shows who owns which state. It treats shared code carefully. It does not only work on the happy path.

That does not mean every pull request has to be tiny or every decision has to be documented in detail. Some features are simply large. Some changes require touching several parts of the application. But even then, the implementation can help the reviewer build confidence instead of making them reconstruct the entire feature from scattered clues.

Code review is not just about finding mistakes. It is about transferring enough understanding that another developer can say, with some confidence, that the change makes sense.

The easier we make that understanding, the better the review becomes.