AgentSkillsCN

frontend-code-style

当您需要编写或审查 JavaScript/TypeScript 代码,以遵循简洁箭头、内联处理器、表达式格式化等代码风格模式,或当您忍不住想使用 eslint-disable 时,可使用此技能。

SKILL.md
--- frontmatter
name: frontend-code-style
description: Use when writing or reviewing JavaScript/TypeScript code for style patterns like concise arrows, inline handlers, expression formatting, or when tempted to use eslint-disable

Frontend: Code Style

Preferred code style patterns for consistent, readable JavaScript/TypeScript code.

Note: These are conventions, not enforced rules. Existing code may not follow these patterns, which is acceptable. When writing new code, prefer these patterns where practical.

Concise Arrow Functions (Preferred)

Preference: When a function body is a single expression, prefer concise arrow syntax without {} braces.

tsx
// Acceptable (existing pattern)
const handleClick = () => {
    doSomething();
};

const getFullName = (user) => {
    return `${user.firstName} ${user.lastName}`;
};

// Preferred for NEW code: Concise form for single expressions
const handleClick = () => doSomething();

const getFullName = (user) => `${user.firstName} ${user.lastName}`;

When braces ARE needed:

tsx
// Multiple statements require braces
const handleSubmit = () => {
    validate();
    submit();
};

// Side effect followed by return
const processAndReturn = (data) => {
    console.log(data);
    return transform(data);
};

Inline Single-Expression Handlers (Preferred)

Preference: For single-expression handlers, prefer inlining directly in the event prop rather than declaring a separate named function.

tsx
// Acceptable (existing pattern)
const handleOpenModal = () => setIsOpen(true);
const handleCloseModal = () => setIsOpen(false);

return (
    <>
        <Button onClick={handleOpenModal}>Open</Button>
        <Modal onClose={handleCloseModal} />
    </>
);

// Preferred for NEW code: Inline directly in the event prop
return (
    <>
        <Button onClick={() => setIsOpen(true)}>Open</Button>
        <Modal onClose={() => setIsOpen(false)} />
    </>
);

When to extract a named handler:

  1. Multiple expressions - Handler does more than one thing

    tsx
    const handleSubmit = () => {
        validateForm();
        submitData();
    };
    
  2. Reused in multiple places - Same handler used by multiple elements

    tsx
    const handleClose = () => {
        resetForm();
        setIsOpen(false);
    };
    
    <Button onClick={handleClose}>Cancel</Button>
    <Button onClick={handleClose}>X</Button>
    
  3. Complex logic - Benefits from a descriptive name for readability

    tsx
    const handleToggleWithAnalytics = () => {
        trackEvent("feature_toggled");
        setIsEnabled((prev) => !prev);
    };
    
  4. Memoization needed - Passed to memoized child component

    tsx
    const handleChange = useCallback((value) => {
        setData(value);
    }, []);
    
    <MemoizedInput onChange={handleChange} />;
    

FORBIDDEN: eslint-disable Comments

NEVER use eslint-disable comments to suppress hook dependency warnings. These warnings exist for good reason - suppressing them hides bugs.

tsx
// ❌ FORBIDDEN - Suppressing eslint is a code smell
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
    pContext.setState({ data: newData });
}, [newData]);

// ❌ Also forbidden
/* eslint-disable react-hooks/exhaustive-deps */

Root Cause: Unstable References

The warning usually means a dependency changes reference on every render. Common causes:

  1. Context object recreated - value={{ state, setState }} creates new object each render
  2. Array/object created inline - options={[1, 2, 3]} is a new array each render
  3. Callback not memoized - Functions created in render without useCallback

Solution: Compare Before Update

Instead of suppressing the warning, add a comparison to break the update cycle:

tsx
// ✅ CORRECT - Keep all dependencies, compare before updating
useEffect(() => {
    // Compare to check if update is actually needed
    const prevIds = pContext.state.users
        .map((u) => u.id)
        .sort()
        .join(",");
    const nextIds = newUsers
        .map((u) => u.id)
        .sort()
        .join(",");
    if (prevIds !== nextIds) {
        pContext.setState({ users: newUsers });
    }
}, [newUsers, pContext]); // All deps included, eslint happy

Solution: Functional Updates

For state that depends on previous value:

tsx
// ✅ CORRECT - Use functional update to avoid needing state in deps
setOnlineUsers((prev) => {
    if (prev.length !== next.length) return next;
    const prevIds = prev
        .map((u) => u.id)
        .sort()
        .join(",");
    const nextIds = next
        .map((u) => u.id)
        .sort()
        .join(",");
    return prevIds === nextIds ? prev : next;
});

Exception Process

If eslint-disable is truly unavoidable:

  1. Stop and ask the user - Explain why you believe it's necessary
  2. Get explicit approval - User must confirm it's acceptable
  3. Document the reason - Add a comment explaining WHY (not just disabling)
tsx
// ⚠️ EXCEPTION - User approved on 2026-01-17
// Reason: Third-party library returns unstable ref, no workaround available
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => { ... }, [stableValue]);

Related Skills

  • frontend-naming-conventions - Naming patterns for components, hooks, and files
<!-- Last updated: 2026-01-17 - Added eslint-disable forbidden section -->