Skip to content

Instantly share code, notes, and snippets.

@monam2
Forked from toy-crane/toss-frontend-rules.mdc
Created May 19, 2025 04:06
Show Gist options
  • Select an option

  • Save monam2/55b172d5649f600b8cdd97be72b297fe to your computer and use it in GitHub Desktop.

Select an option

Save monam2/55b172d5649f600b8cdd97be72b297fe to your computer and use it in GitHub Desktop.

Revisions

  1. @toy-crane toy-crane revised this gist Apr 21, 2025. No changes.
  2. @toy-crane toy-crane created this gist Apr 21, 2025.
    689 changes: 689 additions & 0 deletions toss-frontend-rules.mdc
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,689 @@

    # 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 (
    <AuthGuard>
    {" "}
    {/* Wrapper handles auth check */}
    <LoginStartPage />
    </AuthGuard>
    );
    }

    // 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 */}
    <InviteButton name={data.name} />
    {/* ... other UI ... */}
    </>
    );
    }

    // InviteButton handles the confirmation flow internally
    function InviteButton({ name }) {
    const handleClick = async () => {
    const canInvite = await overlay.openAsync(({ isOpen, close }) => (
    <ConfirmDialog
    title={`Share with ${name}`}
    // ... dialog setup ...
    />
    ));

    if (canInvite) {
    await sendPush();
    }
    };

    return <Button onClick={handleClick}>Invite</Button>;
    }
    ```

    ## 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 ? <ViewerSubmitButton /> : <AdminSubmitButton />;
    }

    // Component specifically for the 'viewer' role
    function ViewerSubmitButton() {
    return <TextButton disabled>Submit</TextButton>;
    }

    // Component specifically for the 'admin' (or non-viewer) role
    function AdminSubmitButton() {
    useEffect(() => {
    showAnimation(); // Animation logic isolated here
    }, []);

    return <Button type="submit">Submit</Button>;
    }
    ```

    ## 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 (
    <div>
    <Button disabled={false}>Invite</Button>
    <Button disabled={false}>View</Button>
    </div>
    );
    case "viewer":
    return (
    <div>
    <Button disabled={true}>Invite</Button> {/* Example for viewer */}
    <Button disabled={false}>View</Button>
    </div>
    );
    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 (
    <div>
    <Button disabled={!policy.canInvite}>Invite</Button>
    <Button disabled={!policy.canView}>View</Button>
    </div>
    );
    }
    ```

    ## 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<UserType>
    function useUser(): UseQueryResult<UserType, Error> {
    const query = useQuery({ queryKey: ["user"], queryFn: fetchUser });
    return query;
    }

    // Assuming fetchServerTime returns Promise<Date>
    function useServerTime(): UseQueryResult<Date, Error> {
    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<number> {
    const balance = await http.get<number>("...");
    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 (
    <form onSubmit={onSubmit}>
    <div>
    <input
    {...register("name", {
    validate: (value) =>
    value.trim() === "" ? "Please enter your name." : true, // Example validation
    })}
    placeholder="Name"
    />
    {errors.name && <p>{errors.name.message}</p>}
    </div>
    <div>
    <input
    {...register("email", {
    validate: (value) =>
    /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value)
    ? true
    : "Invalid email address.", // Example validation
    })}
    placeholder="Email"
    />
    {errors.email && <p>{errors.email.message}</p>}
    </div>
    <button type="submit">Submit</button>
    </form>
    );
    }
    ```

    #### 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 (
    <form onSubmit={onSubmit}>
    <div>
    <input {...register("name")} placeholder="Name" />
    {errors.name && <p>{errors.name.message}</p>}
    </div>
    <div>
    <input {...register("email")} placeholder="Email" />
    {errors.email && <p>{errors.email.message}</p>}
    </div>
    <button type="submit">Submit</button>
    </form>
    );
    }
    ```

    **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 (
    <Modal open={open} onClose={onClose}>
    {/* Input and Button rendered directly */}
    <div
    style={{
    display: "flex",
    justifyContent: "space-between",
    marginBottom: "1rem",
    }}
    >
    <Input
    value={keyword}
    onChange={(e) => setKeyword(e.target.value)} // State managed here
    placeholder="Search items..."
    />
    <Button onClick={onClose}>Close</Button>
    </div>
    {/* ItemEditList rendered directly, gets props it needs */}
    <ItemEditList
    keyword={keyword} // Passed directly
    items={items} // Passed directly
    recommendedItems={recommendedItems} // Passed directly
    onConfirm={onConfirm} // Passed directly
    />
    </Modal>
    );
    }

    // The intermediate ItemEditBody component is eliminated, reducing coupling.
    ```