# Frontend Design Guideline This document summarizes key frontend design principles and rules, showcasing recommended patterns. Follow these guidelines when writing frontend code. # Readability Improving the clarity and ease of understanding code. ## Naming Magic Numbers **Rule:** Replace magic numbers with named constants for clarity. **Reasoning:** - Improves clarity by giving semantic meaning to unexplained values. - Enhances maintainability. #### Recommended Pattern: ```typescript const ANIMATION_DELAY_MS = 300; async function onLikeClick() { await postLike(url); await delay(ANIMATION_DELAY_MS); // Clearly indicates waiting for animation await refetchPostLike(); } ``` ## Abstracting Implementation Details **Rule:** Abstract complex logic/interactions into dedicated components/HOCs. **Reasoning:** - Reduces cognitive load by separating concerns. - Improves readability, testability, and maintainability of components. #### Recommended Pattern 1: Auth Guard (Login check abstracted to a wrapper/guard component) ```tsx // App structure function App() { return ( {" "} {/* Wrapper handles auth check */} ); } // AuthGuard component encapsulates the check/redirect logic function AuthGuard({ children }) { const status = useCheckLoginStatus(); useEffect(() => { if (status === "LOGGED_IN") { location.href = "/home"; } }, [status]); // Render children only if not logged in, otherwise render null (or loading) return status !== "LOGGED_IN" ? children : null; } // LoginStartPage is now simpler, focused only on login UI/logic function LoginStartPage() { // ... login related logic ONLY ... return <>{/* ... login related components ... */}; } ``` #### Recommended Pattern 2: Dedicated Interaction Component (Dialog logic abstracted into a dedicated `InviteButton` component) ```tsx export function FriendInvitation() { const { data } = useQuery(/* ... */); return ( <> {/* Use the dedicated button component */} {/* ... other UI ... */} ); } // InviteButton handles the confirmation flow internally function InviteButton({ name }) { const handleClick = async () => { const canInvite = await overlay.openAsync(({ isOpen, close }) => ( )); if (canInvite) { await sendPush(); } }; return ; } ``` ## Separating Code Paths for Conditional Rendering **Rule:** Separate significantly different conditional UI/logic into distinct components. **Reasoning:** - Improves readability by avoiding complex conditionals within one component. - Ensures each specialized component has a clear, single responsibility. #### Recommended Pattern: (Separate components for each role) ```tsx function SubmitButton() { const isViewer = useRole() === "viewer"; // Delegate rendering to specialized components return isViewer ? : ; } // Component specifically for the 'viewer' role function ViewerSubmitButton() { return Submit; } // Component specifically for the 'admin' (or non-viewer) role function AdminSubmitButton() { useEffect(() => { showAnimation(); // Animation logic isolated here }, []); return ; } ``` ## Simplifying Complex Ternary Operators **Rule:** Replace complex/nested ternaries with `if`/`else` or IIFEs for readability. **Reasoning:** - Makes conditional logic easier to follow quickly. - Improves overall code maintainability. #### Recommended Pattern: (Using an IIFE with `if` statements) ```typescript const status = (() => { if (ACondition && BCondition) return "BOTH"; if (ACondition) return "A"; if (BCondition) return "B"; return "NONE"; })(); ``` ## Reducing Eye Movement (Colocating Simple Logic) **Rule:** Colocate simple, localized logic or use inline definitions to reduce context switching. **Reasoning:** - Allows top-to-bottom reading and faster comprehension. - Reduces cognitive load from context switching (eye movement). #### Recommended Pattern A: Inline `switch` ```tsx function Page() { const user = useUser(); // Logic is directly visible here switch (user.role) { case "admin": return (
); case "viewer": return (
{/* Example for viewer */}
); default: return null; } } ``` #### Recommended Pattern B: Colocated simple policy object ```tsx function Page() { const user = useUser(); // Simple policy defined right here, easy to see const policy = { admin: { canInvite: true, canView: true }, viewer: { canInvite: false, canView: true }, }[user.role]; // Ensure policy exists before accessing properties if role might not match if (!policy) return null; return (
); } ``` ## Naming Complex Conditions **Rule:** Assign complex boolean conditions to named variables. **Reasoning:** - Makes the _meaning_ of the condition explicit. - Improves readability and self-documentation by reducing cognitive load. #### Recommended Pattern: (Conditions assigned to named variables) ```typescript const matchedProducts = products.filter((product) => { // Check if product belongs to the target category const isSameCategory = product.categories.some( (category) => category.id === targetCategory.id ); // Check if any product price falls within the desired range const isPriceInRange = product.prices.some( (price) => price >= minPrice && price <= maxPrice ); // The overall condition is now much clearer return isSameCategory && isPriceInRange; }); ``` **Guidance:** Name conditions when the logic is complex, reused, or needs unit testing. Avoid naming very simple, single-use conditions. # Predictability Ensuring code behaves as expected based on its name, parameters, and context. ## Standardizing Return Types **Rule:** Use consistent return types for similar functions/hooks. **Reasoning:** - Improves code predictability; developers can anticipate return value shapes. - Reduces confusion and potential errors from inconsistent types. #### Recommended Pattern 1: API Hooks (React Query) ```typescript // Always return the Query object import { useQuery, UseQueryResult } from "@tanstack/react-query"; // Assuming fetchUser returns Promise function useUser(): UseQueryResult { const query = useQuery({ queryKey: ["user"], queryFn: fetchUser }); return query; } // Assuming fetchServerTime returns Promise function useServerTime(): UseQueryResult { const query = useQuery({ queryKey: ["serverTime"], queryFn: fetchServerTime, }); return query; } ``` #### Recommended Pattern 2: Validation Functions (Using a consistent type, ideally a Discriminated Union) ```typescript type ValidationResult = { ok: true } | { ok: false; reason: string }; function checkIsNameValid(name: string): ValidationResult { if (name.length === 0) return { ok: false, reason: "Name cannot be empty." }; if (name.length >= 20) return { ok: false, reason: "Name cannot be longer than 20 characters." }; return { ok: true }; } function checkIsAgeValid(age: number): ValidationResult { if (!Number.isInteger(age)) return { ok: false, reason: "Age must be an integer." }; if (age < 18) return { ok: false, reason: "Age must be 18 or older." }; if (age > 99) return { ok: false, reason: "Age must be 99 or younger." }; return { ok: true }; } // Usage allows safe access to 'reason' only when ok is false const nameValidation = checkIsNameValid(name); if (!nameValidation.ok) { console.error(nameValidation.reason); } ``` ## Revealing Hidden Logic (Single Responsibility) **Rule:** Avoid hidden side effects; functions should only perform actions implied by their signature (SRP). **Reasoning:** - Leads to predictable behavior without unintended side effects. - Creates more robust, testable code through separation of concerns (SRP). #### Recommended Pattern: ```typescript // Function *only* fetches balance async function fetchBalance(): Promise { const balance = await http.get("..."); return balance; } // Caller explicitly performs logging where needed async function handleUpdateClick() { const balance = await fetchBalance(); // Fetch logging.log("balance_fetched"); // Log (explicit action) await syncBalance(balance); // Another action } ``` ## Using Unique and Descriptive Names (Avoiding Ambiguity) **Rule:** Use unique, descriptive names for custom wrappers/functions to avoid ambiguity. **Reasoning:** - Avoids ambiguity and enhances predictability. - Allows developers to understand specific actions (e.g., adding auth) directly from the name. #### Recommended Pattern: ```typescript // In httpService.ts - Clearer module name import { http as httpLibrary } from "@some-library/http"; export const httpService = { // Unique module name async getWithAuth(url: string) { // Descriptive function name const token = await fetchToken(); return httpLibrary.get(url, { headers: { Authorization: `Bearer ${token}` }, }); }, }; // In fetchUser.ts - Usage clearly indicates auth import { httpService } from "./httpService"; export async function fetchUser() { // Name 'getWithAuth' makes the behavior explicit return await httpService.getWithAuth("..."); } ``` # Cohesion Keeping related code together and ensuring modules have a well-defined, single purpose. ## Considering Form Cohesion **Rule:** Choose field-level or form-level cohesion based on form requirements. **Reasoning:** - Balances field independence (field-level) vs. form unity (form-level). - Ensures related form logic is appropriately grouped based on requirements. #### Recommended Pattern (Field-Level Example): ```tsx // Each field uses its own `validate` function import { useForm } from "react-hook-form"; export function Form() { const { register, formState: { errors }, handleSubmit, } = useForm({ /* defaultValues etc. */ }); const onSubmit = handleSubmit((formData) => { console.log("Form submitted:", formData); }); return (
value.trim() === "" ? "Please enter your name." : true, // Example validation })} placeholder="Name" /> {errors.name &&

{errors.name.message}

}
/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value) ? true : "Invalid email address.", // Example validation })} placeholder="Email" /> {errors.email &&

{errors.email.message}

}
); } ``` #### Recommended Pattern (Form-Level Example): ```tsx // A single schema defines validation for the whole form import * as z from "zod"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; const schema = z.object({ name: z.string().min(1, "Please enter your name."), email: z.string().min(1, "Please enter your email.").email("Invalid email."), }); export function Form() { const { register, formState: { errors }, handleSubmit, } = useForm({ resolver: zodResolver(schema), defaultValues: { name: "", email: "" }, }); const onSubmit = handleSubmit((formData) => { console.log("Form submitted:", formData); }); return (
{errors.name &&

{errors.name.message}

}
{errors.email &&

{errors.email.message}

}
); } ``` **Guidance:** Choose **field-level** for independent validation, async checks, or reusable fields. Choose **form-level** for related fields, wizard forms, or interdependent validation. ## Organizing Code by Feature/Domain **Rule:** Organize directories by feature/domain, not just by code type. **Reasoning:** - Increases cohesion by keeping related files together. - Simplifies feature understanding, development, maintenance, and deletion. #### Recommended Pattern: (Organized by feature/domain) ``` src/ ├── components/ # Shared/common components ├── hooks/ # Shared/common hooks ├── utils/ # Shared/common utils ├── domains/ │ ├── user/ │ │ ├── components/ │ │ │ └── UserProfileCard.tsx │ │ ├── hooks/ │ │ │ └── useUser.ts │ │ └── index.ts # Optional barrel file │ ├── product/ │ │ ├── components/ │ │ │ └── ProductList.tsx │ │ ├── hooks/ │ │ │ └── useProducts.ts │ │ └── ... │ └── order/ │ ├── components/ │ │ └── OrderSummary.tsx │ ├── hooks/ │ │ └── useOrder.ts │ └── ... └── App.tsx ``` ## Relating Magic Numbers to Logic **Rule:** Define constants near related logic or ensure names link them clearly. **Reasoning:** - Improves cohesion by linking constants to the logic they represent. - Prevents silent failures caused by updating logic without updating related constants. #### Recommended Pattern: ```typescript // Constant clearly named and potentially defined near animation logic const ANIMATION_DELAY_MS = 300; async function onLikeClick() { await postLike(url); // Delay uses the constant, maintaining the link to the animation await delay(ANIMATION_DELAY_MS); await refetchPostLike(); } ``` _Ensure constants are maintained alongside the logic they depend on or clearly named to show the relationship._ # Coupling Minimizing dependencies between different parts of the codebase. ## Balancing Abstraction and Coupling (Avoiding Premature Abstraction) **Rule:** Avoid premature abstraction of duplicates if use cases might diverge; prefer lower coupling. **Reasoning:** - Avoids tight coupling from forcing potentially diverging logic into one abstraction. - Allowing some duplication can improve decoupling and maintainability when future needs are uncertain. #### Guidance: Before abstracting, consider if the logic is truly identical and likely to _stay_ identical across all use cases. If divergence is possible (e.g., different pages needing slightly different behavior from a shared hook like `useOpenMaintenanceBottomSheet`), keeping the logic separate initially (allowing duplication) can lead to more maintainable, decoupled code. Discuss trade-offs with the team. _[No specific 'good' code example here, as the recommendation is situational awareness rather than a single pattern]._ ## Scoping State Management (Avoiding Overly Broad Hooks) **Rule:** Break down broad state management into smaller, focused hooks/contexts. **Reasoning:** - Reduces coupling by ensuring components only depend on necessary state slices. - Improves performance by preventing unnecessary re-renders from unrelated state changes. #### Recommended Pattern: (Focused hooks, low coupling) ```typescript // Hook specifically for cardId query param import { useQueryParam, NumberParam } from "use-query-params"; import { useCallback } from "react"; export function useCardIdQueryParam() { // Assuming 'query' provides the raw param value const [cardIdParam, setCardIdParam] = useQueryParam("cardId", NumberParam); const setCardId = useCallback( (newCardId: number | undefined) => { setCardIdParam(newCardId, "replaceIn"); // Or 'push' depending on desired history behavior }, [setCardIdParam] ); // Provide a stable return tuple return [cardIdParam ?? undefined, setCardId] as const; } // Separate hook for date range, etc. // export function useDateRangeQueryParam() { /* ... */ } ``` Components now only import and use `useCardIdQueryParam` if they need `cardId`, decoupling them from date range state, etc. ## Eliminating Props Drilling with Composition **Rule:** Use Component Composition instead of Props Drilling. **Reasoning:** - Significantly reduces coupling by eliminating unnecessary intermediate dependencies. - Makes refactoring easier and clarifies data flow in flatter component trees. #### Recommended Pattern: ```tsx import React, { useState } from "react"; // Assume Modal, Input, Button, ItemEditList components exist function ItemEditModal({ open, items, recommendedItems, onConfirm, onClose }) { const [keyword, setKeyword] = useState(""); // Render children directly within Modal, passing props only where needed return ( {/* Input and Button rendered directly */}
setKeyword(e.target.value)} // State managed here placeholder="Search items..." />
{/* ItemEditList rendered directly, gets props it needs */}
); } // The intermediate ItemEditBody component is eliminated, reducing coupling. ```