AgentSkillsCN

Frontend Fundamental

前端基础

SKILL.md

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)

code
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.