Frontend Best Practices
Improve frontend code without treating any pattern as universally correct. Preserve behavior, follow repository conventions, and optimize for correctness, clarity, accessibility, and maintainability before cleverness.
Workflow
- Read repository instructions, framework documentation, package versions, and nearby implementations before editing.
- Identify the requested outcome, current behavior, public interfaces, and constraints.
- Inspect the nearest established pattern and reuse existing components, utilities, tokens, and dependencies.
- Choose the smallest design that solves the current problem and leaves a clear extension path.
- Implement cohesive changes with explicit types at module boundaries.
- Verify behavior, accessibility, type safety, linting, and relevant tests in proportion to risk. When a targeted unit is updated, update or add its unit tests in the same change unless the unit is intentionally untestable and explain why.
- Report the outcome and distinguish new failures from pre-existing repository failures.
Architecture
- Organize by feature or domain when code changes together; keep generic primitives in a shared layer.
- Keep data access, stateful orchestration, and presentation separable when doing so clarifies ownership or enables testing.
- Prefer composition and focused hooks over deeply configurable components.
- Keep state as local as possible. Lift it only when multiple consumers need a shared source of truth.
- Derive values during render when possible; do not synchronize derivable state with effects.
- Avoid speculative abstractions. Extract after a stable repeated concept appears, not merely repeated syntax.
- Preserve server/client boundaries. Do not promote a large subtree to client code for one interactive leaf.
Components and Hooks
- Give each component one coherent responsibility, without enforcing arbitrary line limits.
- Prefer explicit props and children composition. Avoid boolean-prop combinations that create invalid states.
- Use discriminated unions when component modes require different props.
- Keep hook dependencies correct. Stabilize values only when identity has an observable cost or semantic requirement.
- Use effects only to synchronize with external systems; put user-triggered work in event handlers.
- Provide loading, empty, error, disabled, and success states where the workflow requires them.
Component Modularity and Extraction
- Start with local components and extract when a section has a distinct responsibility, meaningful internal logic, independent state, repeated use, or a clear testing boundary.
- Keep tightly coupled markup together when extraction would only move JSX, create pass-through props, or force readers to jump between files without clarifying ownership.
- Move non-trivial component logic into custom hooks by default, especially form orchestration, state transitions, effects, subscriptions, async work, permission or visibility decisions, and repeated event-handler logic.
- Keep tiny pure render-time derivations inline or as local functions when a hook would only add indirection.
- Prefer small composed components over one highly configurable component with many boolean props and conditional branches.
- Place one-off feature components beside their parent. Promote them to the feature boundary only when multiple feature files consume them, and to shared UI only when they are domain-neutral and reused across features.
- Give extracted components explicit, minimal props based on the child responsibility. Pass domain objects when the child owns that domain concept; avoid forwarding a large parent state object for convenience.
- Keep server data loading and authorization in server components where possible, then pass serializable data to focused client components that own interaction.
- Avoid using file length or JSX line count as the extraction rule. Extract to improve ownership, reuse, testability, or readability—not to satisfy an arbitrary size limit.
- After extraction, check that dependencies flow in one direction, server/client boundaries remain intentional, names describe responsibilities, and the parent reads as a clear composition of the feature.
Barrel Exports
Use barrel files deliberately:
- Use a feature-level
index.tsto expose a small, intentional public API and hide internal files. - Prefer named exports and type-only exports where appropriate.
- Avoid broad
export *chains across many directories; they obscure ownership and invite naming collisions. - Avoid barrels in dependency-cycle hotspots, initialization-sensitive modules, and server/client boundary code.
- Import from the feature barrel outside the feature; use direct relative imports inside the feature when that reduces cycles.
- Verify the bundler handles tree shaking before claiming barrels are performance-neutral.
Example public boundary:
export { AuditTable } from "./audit-table";
export type { AuditEvent, AuditCategory } from "./types";
Higher-Order Components
Use an HOC when behavior must wrap many existing components or a framework API requires one. Prefer hooks or composition for new local behavior.
When implementing an HOC:
- Preserve the wrapped component's props with generics.
- Forward refs only when consumers need the underlying ref.
- Set a useful
displayNamefor debugging. - Do not mutate the wrapped component or silently overwrite unrelated props.
- Keep injected props explicit and prevent callers from supplying conflicting values.
- Hoist non-React statics only when consumers rely on them and use a proven utility.
function withPermission<P extends object>(Component: React.ComponentType<P>) {
function WithPermission(props: P) {
return <Component {...props} />;
}
WithPermission.displayName = `withPermission(${Component.displayName ?? Component.name ?? "Component"})`;
return WithPermission;
}
TypeScript and Data
- Model domain states precisely; avoid
any, unchecked assertions, and duplicated interfaces. - Validate untrusted data at system boundaries. Static types do not validate runtime input.
- Keep transport models separate from view models when their shapes or lifecycles differ.
- Use stable identifiers for list keys; never use an array index when items can reorder.
- Treat URL state as the source of truth for shareable filters, pagination, tabs, and searches when appropriate.
Testing
- When changing a targeted unit, inspect nearby existing tests before editing and update or add unit tests for changed behavior in the same change.
- Prefer focused tests for business logic, validation, permissions, data transformation, hooks, and API boundaries.
- Keep tests user-facing where possible; avoid asserting third-party internals, implementation details, or styling that is not business-critical.
- If no test is added for a changed unit, state the reason clearly in the handoff.
Accessibility and UX
- Use semantic HTML before ARIA. Give every control an accessible name and visible focus state.
- Preserve keyboard operation, logical focus order, and focus restoration for dialogs and menus.
- Associate validation errors and instructions with their inputs.
- Do not rely on color alone to communicate status.
- Respect reduced motion and avoid unexpected layout shifts.
- Make responsive behavior work at narrow widths, zoomed layouts, and long localized text.
Performance
- Measure or identify a credible bottleneck before optimizing.
- Prevent request waterfalls and duplicate fetching; choose caching based on freshness requirements.
- Keep rendering work proportional to visible UI. Paginate or virtualize genuinely large collections.
- Memoize expensive computations or identity-sensitive values, not every function and object.
- Lazy-load large optional experiences, while keeping critical interactions immediately available.
- Avoid importing large modules through convenience APIs when a direct import materially reduces the client bundle.
Review Checklist
- Confirm behavior and edge cases match the request.
- Check component responsibilities, extraction boundaries, naming, duplication, dead code, and accidental API changes.
- Check semantic HTML, keyboard support, focus, labels, contrast, and status announcements.
- Check async cancellation, race conditions, stale closures, error recovery, and optimistic rollback.
- Check unnecessary client rendering, effects, rerenders, network calls, and bundle growth.
- Run focused tests first, then broader lint, type, and build checks when practical.
- Do not mix unrelated cleanup into a focused change.
Decision Priority
Resolve conflicts in this order:
- User requirements and repository instructions
- Correctness and security
- Accessibility
- Maintainability and consistency
- Performance supported by evidence
- Personal style preference