安装
npx skills add https://github.com/supercent-io/skills-template --skill ui-component-patterns
- UI Component Patterns
- When to use this skill
- Building Component Libraries
-
- Creating reusable UI components
- Implementing Design Systems
-
- Applying consistent UI patterns
- Complex UI
-
- Components requiring multiple variants (Button, Modal, Dropdown)
- Refactoring
-
- Extracting duplicate code into components
- Instructions
- Step 1: Props API Design
- Design Props that are easy to use and extensible.
- Principles
- :
- Clear names
- Reasonable defaults
- Type definitions with TypeScript
- Optional Props use optional marker (?)
- Example
- (Button):
- interface
- ButtonProps
- {
- // Required
- children
- :
- React
- .
- ReactNode
- ;
- // Optional (with defaults)
- variant
- ?
- :
- 'primary'
- |
- 'secondary'
- |
- 'outline'
- |
- 'ghost'
- ;
- size
- ?
- :
- 'sm'
- |
- 'md'
- |
- 'lg'
- ;
- disabled
- ?
- :
- boolean
- ;
- isLoading
- ?
- :
- boolean
- ;
- // Event handlers
- onClick
- ?
- :
- (
- event
- :
- React
- .
- MouseEvent
- <
- HTMLButtonElement
- >
- )
- =>
- void
- ;
- // HTML attribute inheritance
- type
- ?
- :
- 'button'
- |
- 'submit'
- |
- 'reset'
- ;
- className
- ?
- :
- string
- ;
- }
- function
- Button
- (
- {
- children
- ,
- variant
- =
- 'primary'
- ,
- size
- =
- 'md'
- ,
- disabled
- =
- false
- ,
- isLoading
- =
- false
- ,
- onClick
- ,
- type
- =
- 'button'
- ,
- className
- =
- ''
- ,
- ...
- rest
- }
- :
- ButtonProps
- )
- {
- const
- baseClasses
- =
- 'btn'
- ;
- const
- variantClasses
- =
- `
- btn-
- ${
- variant
- }
- `
- ;
- const
- sizeClasses
- =
- `
- btn-
- ${
- size
- }
- `
- ;
- const
- classes
- =
- `
- ${
- baseClasses
- }
- ${
- variantClasses
- }
- ${
- sizeClasses
- }
- ${
- className
- }
- `
- ;
- return
- (
- <
- button
- type
- =
- {
- type
- }
- className
- =
- {
- classes
- }
- disabled
- =
- {
- disabled
- ||
- isLoading
- }
- onClick
- =
- {
- onClick
- }
- {
- ...
- rest
- }
- >
- {
- isLoading
- ?
- <
- Spinner
- />
- :
- children
- }
- </
- button
- >
- )
- ;
- }
- // Usage example
- <
- Button
- variant
- =
- "
- primary
- "
- size
- =
- "
- lg
- "
- onClick
- =
- {
- (
- )
- =>
- alert
- (
- 'Clicked!'
- )
- }
- >
- Click Me
- </
- Button
- >
- Step 2: Composition Pattern
- Combine small components to build complex UI.
- Example
- (Card):
- // Card component (Container)
- interface
- CardProps
- {
- children
- :
- React
- .
- ReactNode
- ;
- className
- ?
- :
- string
- ;
- }
- function
- Card
- (
- {
- children
- ,
- className
- =
- ''
- }
- :
- CardProps
- )
- {
- return
- <
- div
- className
- =
- {
- `
- card
- ${
- className
- }
- `
- }
- >
- {
- children
- }
- </
- div
- >
- ;
- }
- // Card.Header
- function
- CardHeader
- (
- {
- children
- }
- :
- {
- children
- :
- React
- .
- ReactNode
- }
- )
- {
- return
- <
- div
- className
- =
- "
- card-header
- "
- >
- {
- children
- }
- </
- div
- >
- ;
- }
- // Card.Body
- function
- CardBody
- (
- {
- children
- }
- :
- {
- children
- :
- React
- .
- ReactNode
- }
- )
- {
- return
- <
- div
- className
- =
- "
- card-body
- "
- >
- {
- children
- }
- </
- div
- >
- ;
- }
- // Card.Footer
- function
- CardFooter
- (
- {
- children
- }
- :
- {
- children
- :
- React
- .
- ReactNode
- }
- )
- {
- return
- <
- div
- className
- =
- "
- card-footer
- "
- >
- {
- children
- }
- </
- div
- >
- ;
- }
- // Compound Component pattern
- Card
- .
- Header
- =
- CardHeader
- ;
- Card
- .
- Body
- =
- CardBody
- ;
- Card
- .
- Footer
- =
- CardFooter
- ;
- export
- default
- Card
- ;
- // Usage
- import
- Card
- from
- './Card'
- ;
- function
- ProductCard
- (
- )
- {
- return
- (
- <
- Card
- >
- <
- Card.Header
- >
- <
- h3
- >
- Product Name
- </
- h3
- >
- </
- Card.Header
- >
- <
- Card.Body
- >
- <
- img
- src
- =
- "
- ...
- "
- alt
- =
- "
- Product
- "
- />
- <
- p
- >
- Product description here...
- </
- p
- >
- </
- Card.Body
- >
- <
- Card.Footer
- >
- <
- button
- >
- Add to Cart
- </
- button
- >
- </
- Card.Footer
- >
- </
- Card
- >
- )
- ;
- }
- Step 3: Render Props / Children as Function
- A pattern for flexible customization.
- Example
- (Dropdown):
- interface
- DropdownProps
- <
- T
- >
- {
- items
- :
- T
- [
- ]
- ;
- renderItem
- :
- (
- item
- :
- T
- ,
- index
- :
- number
- )
- =>
- React
- .
- ReactNode
- ;
- onSelect
- :
- (
- item
- :
- T
- )
- =>
- void
- ;
- placeholder
- ?
- :
- string
- ;
- }
- function
- Dropdown
- <
- T
- >
- (
- {
- items
- ,
- renderItem
- ,
- onSelect
- ,
- placeholder
- }
- :
- DropdownProps
- <
- T
- >
- )
- {
- const
- [
- isOpen
- ,
- setIsOpen
- ]
- =
- useState
- (
- false
- )
- ;
- const
- [
- selected
- ,
- setSelected
- ]
- =
- useState
- <
- T
- |
- null
- >
- (
- null
- )
- ;
- const
- handleSelect
- =
- (
- item
- :
- T
- )
- =>
- {
- setSelected
- (
- item
- )
- ;
- onSelect
- (
- item
- )
- ;
- setIsOpen
- (
- false
- )
- ;
- }
- ;
- return
- (
- <
- div
- className
- =
- "
- dropdown
- "
- >
- <
- button
- onClick
- =
- {
- (
- )
- =>
- setIsOpen
- (
- !
- isOpen
- )
- }
- >
- {
- selected
- ?
- renderItem
- (
- selected
- ,
- -
- 1
- )
- :
- placeholder
- ||
- 'Select...'
- }
- </
- button
- >
- {
- isOpen
- &&
- (
- <
- ul
- className
- =
- "
- dropdown-menu
- "
- >
- {
- items
- .
- map
- (
- (
- item
- ,
- index
- )
- =>
- (
- <
- li
- key
- =
- {
- index
- }
- onClick
- =
- {
- (
- )
- =>
- handleSelect
- (
- item
- )
- }
- >
- {
- renderItem
- (
- item
- ,
- index
- )
- }
- </
- li
- >
- )
- )
- }
- </
- ul
- >
- )
- }
- </
- div
- >
- )
- ;
- }
- // Usage
- interface
- User
- {
- id
- :
- string
- ;
- name
- :
- string
- ;
- avatar
- :
- string
- ;
- }
- function
- UserDropdown
- (
- )
- {
- const
- users
- :
- User
- [
- ]
- =
- [
- ...
- ]
- ;
- return
- (
- <
- Dropdown
- items
- =
- {
- users
- }
- placeholder
- =
- "
- Select a user
- "
- renderItem
- =
- {
- (
- user
- )
- =>
- (
- <
- div
- className
- =
- "
- user-item
- "
- >
- <
- img
- src
- =
- {
- user
- .
- avatar
- }
- alt
- =
- {
- user
- .
- name
- }
- />
- <
- span
- >
- {
- user
- .
- name
- }
- </
- span
- >
- </
- div
- >
- )
- }
- onSelect
- =
- {
- (
- user
- )
- =>
- console
- .
- log
- (
- 'Selected:'
- ,
- user
- )
- }
- />
- )
- ;
- }
- Step 4: Separating Logic with Custom Hooks
- Separate UI from business logic.
- Example
- (Modal):
- // hooks/useModal.ts
- function
- useModal
- (
- initialOpen
- =
- false
- )
- {
- const
- [
- isOpen
- ,
- setIsOpen
- ]
- =
- useState
- (
- initialOpen
- )
- ;
- const
- open
- =
- useCallback
- (
- (
- )
- =>
- setIsOpen
- (
- true
- )
- ,
- [
- ]
- )
- ;
- const
- close
- =
- useCallback
- (
- (
- )
- =>
- setIsOpen
- (
- false
- )
- ,
- [
- ]
- )
- ;
- const
- toggle
- =
- useCallback
- (
- (
- )
- =>
- setIsOpen
- (
- prev
- =>
- !
- prev
- )
- ,
- [
- ]
- )
- ;
- return
- {
- isOpen
- ,
- open
- ,
- close
- ,
- toggle
- }
- ;
- }
- // components/Modal.tsx
- interface
- ModalProps
- {
- isOpen
- :
- boolean
- ;
- onClose
- :
- (
- )
- =>
- void
- ;
- title
- :
- string
- ;
- children
- :
- React
- .
- ReactNode
- ;
- }
- function
- Modal
- (
- {
- isOpen
- ,
- onClose
- ,
- title
- ,
- children
- }
- :
- ModalProps
- )
- {
- if
- (
- !
- isOpen
- )
- return
- null
- ;
- return
- (
- <
- div
- className
- =
- "
- modal-overlay
- "
- onClick
- =
- {
- onClose
- }
- >
- <
- div
- className
- =
- "
- modal-content
- "
- onClick
- =
- {
- (
- e
- )
- =>
- e
- .
- stopPropagation
- (
- )
- }
- >
- <
- div
- className
- =
- "
- modal-header
- "
- >
- <
- h2
- >
- {
- title
- }
- </
- h2
- >
- <
- button
- onClick
- =
- {
- onClose
- }
- aria-label
- =
- "
- Close
- "
- >
- ×
- </
- button
- >
- </
- div
- >
- <
- div
- className
- =
- "
- modal-body
- "
- >
- {
- children
- }
- </
- div
- >
- </
- div
- >
- </
- div
- >
- )
- ;
- }
- // Usage
- function
- App
- (
- )
- {
- const
- {
- isOpen
- ,
- open
- ,
- close
- }
- =
- useModal
- (
- )
- ;
- return
- (
- <
- >
- <
- button
- onClick
- =
- {
- open
- }
- >
- Open Modal
- </
- button
- >
- <
- Modal
- isOpen
- =
- {
- isOpen
- }
- onClose
- =
- {
- close
- }
- title
- =
- "
- My Modal
- "
- >
- <
- p
- >
- Modal content here...
- </
- p
- >
- </
- Modal
- >
- </
- >
- )
- ;
- }
- Step 5: Performance Optimization
- Prevent unnecessary re-renders.
- React.memo
- :
- // ❌ Bad: child re-renders every time parent re-renders
- function
- ExpensiveComponent
- (
- {
- data
- }
- )
- {
- console
- .
- log
- (
- 'Rendering...'
- )
- ;
- return
- <
- div
- >
- {
- / Complex UI /
- }
- </
- div
- >
- ;
- }
- // ✅ Good: re-renders only when props change
- const
- ExpensiveComponent
- =
- React
- .
- memo
- (
- (
- {
- data
- }
- )
- =>
- {
- console
- .
- log
- (
- 'Rendering...'
- )
- ;
- return
- <
- div
- >
- {
- / Complex UI /
- }
- </
- div
- >
- ;
- }
- )
- ;
- useMemo & useCallback
- :
- function
- ProductList
- (
- {
- products
- ,
- category
- }
- :
- {
- products
- :
- Product
- [
- ]
- ;
- category
- :
- string
- }
- )
- {
- // ✅ Memoize filtered results
- const
- filteredProducts
- =
- useMemo
- (
- (
- )
- =>
- {
- return
- products
- .
- filter
- (
- p
- =>
- p
- .
- category
- ===
- category
- )
- ;
- }
- ,
- [
- products
- ,
- category
- ]
- )
- ;
- // ✅ Memoize callback
- const
- handleAddToCart
- =
- useCallback
- (
- (
- productId
- :
- string
- )
- =>
- {
- // Add to cart
- console
- .
- log
- (
- 'Adding:'
- ,
- productId
- )
- ;
- }
- ,
- [
- ]
- )
- ;
- return
- (
- <
- div
- >
- {
- filteredProducts
- .
- map
- (
- product
- =>
- (
- <
- ProductCard
- key
- =
- {
- product
- .
- id
- }
- product
- =
- {
- product
- }
- onAddToCart
- =
- {
- handleAddToCart
- }
- />
- )
- )
- }
- </
- div
- >
- )
- ;
- }
- const
- ProductCard
- =
- React
- .
- memo
- (
- (
- {
- product
- ,
- onAddToCart
- }
- )
- =>
- {
- return
- (
- <
- div
- >
- <
- h3
- >
- {
- product
- .
- name
- }
- </
- h3
- >
- <
- button
- onClick
- =
- {
- (
- )
- =>
- onAddToCart
- (
- product
- .
- id
- )
- }
- >
- Add to Cart
- </
- button
- >
- </
- div
- >
- )
- ;
- }
- )
- ;
- Output format
- Component File Structure
- components/
- ├── Button/
- │ ├── Button.tsx # Main component
- │ ├── Button.test.tsx # Tests
- │ ├── Button.stories.tsx # Storybook
- │ ├── Button.module.css # Styles
- │ └── index.ts # Export
- ├── Card/
- │ ├── Card.tsx
- │ ├── CardHeader.tsx
- │ ├── CardBody.tsx
- │ ├── CardFooter.tsx
- │ └── index.ts
- └── Modal/
- ├── Modal.tsx
- ├── useModal.ts # Custom hook
- └── index.ts
- Component Template
- import
- React
- from
- 'react'
- ;
- export
- interface
- ComponentProps
- {
- // Props definition
- children
- :
- React
- .
- ReactNode
- ;
- className
- ?
- :
- string
- ;
- }
- /**
- * Component description
- *
- * @example
- * ```tsx
- * Hello
- * ```
- */
- export
- const
- Component
- =
- React
- .
- forwardRef
- <
- HTMLDivElement
- ,
- ComponentProps
- >
- (
- (
- {
- children
- ,
- className
- =
- ''
- ,
- ...
- rest
- }
- ,
- ref
- )
- =>
- {
- return
- (
- <
- div
- ref
- =
- {
- ref
- }
- className
- =
- {
- `
- component
- ${
- className
- }
- `
- }
- {
- ...
- rest
- }
- >
- {
- children
- }
- </
- div
- >
- )
- ;
- }
- )
- ;
- Component
- .
- displayName
- =
- 'Component'
- ;
- export
- default
- Component
- ;
- Constraints
- Required Rules (MUST)
- Single Responsibility Principle
-
- One component has one role only
- Button handles buttons only, Form handles forms only
- Props Type Definition
-
- TypeScript interface required
- Enables auto-completion
- Type safety
- Accessibility
-
- aria-*, role, tabindex, etc.
- Prohibited Rules (MUST NOT)
- Excessive props drilling
-
- Prohibited when 5+ levels deep
- Use Context or Composition
- No Business Logic
-
- Prohibit API calls and complex calculations in UI components
- Separate into custom hooks
- Inline objects/functions
-
- Performance degradation
- // ❌ Bad example
- <
- Component
- style
- =
- {
- {
- color
- :
- 'red'
- }
- }
- onClick
- =
- {
- (
- )
- =>
- handleClick
- (
- )
- }
- />
- // ✅ Good example
- const
- style
- =
- {
- color
- :
- 'red'
- }
- ;
- const
- handleClick
- =
- useCallback
- (
- (
- )
- =>
- {
- ...
- }
- ,
- [
- ]
- )
- ;
- <
- Component
- style
- =
- {
- style
- }
- onClick
- =
- {
- handleClick
- }
- />
- Examples
- Example 1: Accordion (Compound Component)
- import
- React
- ,
- {
- createContext
- ,
- useContext
- ,
- useState
- }
- from
- 'react'
- ;
- // Share state with Context
- const
- AccordionContext
- =
- createContext
- <
- {
- activeIndex
- :
- number
- |
- null
- ;
- setActiveIndex
- :
- (
- index
- :
- number
- |
- null
- )
- =>
- void
- ;
- }
- |
- null
- >
- (
- null
- )
- ;
- function
- Accordion
- (
- {
- children
- }
- :
- {
- children
- :
- React
- .
- ReactNode
- }
- )
- {
- const
- [
- activeIndex
- ,
- setActiveIndex
- ]
- =
- useState
- <
- number
- |
- null
- >
- (
- null
- )
- ;
- return
- (
- <
- AccordionContext.Provider
- value
- =
- {
- {
- activeIndex
- ,
- setActiveIndex
- }
- }
- >
- <
- div
- className
- =
- "
- accordion
- "
- >
- {
- children
- }
- </
- div
- >
- </
- AccordionContext.Provider
- >
- )
- ;
- }
- function
- AccordionItem
- (
- {
- index
- ,
- title
- ,
- children
- }
- :
- {
- index
- :
- number
- ;
- title
- :
- string
- ;
- children
- :
- React
- .
- ReactNode
- ;
- }
- )
- {
- const
- context
- =
- useContext
- (
- AccordionContext
- )
- ;
- if
- (
- !
- context
- )
- throw
- new
- Error
- (
- 'AccordionItem must be used within Accordion'
- )
- ;
- const
- {
- activeIndex
- ,
- setActiveIndex
- }
- =
- context
- ;
- const
- isActive
- =
- activeIndex
- ===
- index
- ;
- return
- (
- <
- div
- className
- =
- "
- accordion-item
- "
- >
- <
- button
- className
- =
- "
- accordion-header
- "
- onClick
- =
- {
- (
- )
- =>
- setActiveIndex
- (
- isActive
- ?
- null
- :
- index
- )
- }
- aria-expanded
- =
- {
- isActive
- }
- >
- {
- title
- }
- </
- button
- >
- {
- isActive
- &&
- <
- div
- className
- =
- "
- accordion-body
- "
- >
- {
- children
- }
- </
- div
- >
- }
- </
- div
- >
- )
- ;
- }
- Accordion
- .
- Item
- =
- AccordionItem
- ;
- export
- default
- Accordion
- ;
- // Usage
- <
- Accordion
- >
- <
- Accordion.Item
- index
- =
- {
- 0
- }
- title
- =
- "
- Section 1
- "
- >
- Content for section 1
- </
- Accordion.Item
- >
- <
- Accordion.Item
- index
- =
- {
- 1
- }
- title
- =
- "
- Section 2
- "
- >
- Content for section 2
- </
- Accordion.Item
- >
- </
- Accordion
- >
- Example 2: Polymorphic Component (as prop)
- type
- PolymorphicComponentProps
- <
- C
- extends
- React
- .
- ElementType
- >
- =
- {
- as
- ?
- :
- C
- ;
- children
- :
- React
- .
- ReactNode
- ;
- }
- &
- React
- .
- ComponentPropsWithoutRef
- <
- C
- >
- ;
- function
- Text
- <
- C
- extends
- React
- .
- ElementType
- =
- 'span'
- >
- (
- {
- as
- ,
- children
- ,
- ...
- rest
- }
- :
- PolymorphicComponentProps
- <
- C
- >
- )
- {
- const
- Component
- =
- as
- ||
- 'span'
- ;
- return
- <
- Component
- {
- ...
- rest
- }
- >
- {
- children
- }
- </
- Component
- >
- ;
- }
- // Usage
- <
- Text
- >
- Default span
- </
- Text
- >
- <
- Text
- as
- =
- "
- h1
- "
- >
- Heading 1
- </
- Text
- >
- <
- Text
- as
- =
- "
- p
- "
- style
- =
- {
- {
- color
- :
- 'blue'
- }
- }
- >
- Paragraph
- </
- Text
- >
- <
- Text
- as
- =
- {
- Link
- }
- href
- =
- "
- /about
- "
- >
- Link
- </
- Text
- >
- Best practices
- Composition over Props
-
- Leverage children instead of many props
- Controlled vs Uncontrolled
-
- Choose based on situation
- Default Props
-
- Provide reasonable defaults
- Storybook
-
- Component documentation and development
- References
- React Patterns
- Compound Components
- Radix UI
- - Accessible components
- Chakra UI
- - Component library
- shadcn/ui
- - Copy-paste components
- Metadata
- Version
- Current Version
-
- 1.0.0
- Last Updated
-
- 2025-01-01
- Compatible Platforms
- Claude, ChatGPT, Gemini
← 返回排行榜