ui-component-patterns

安装量: 10.5K
排名: #218

安装

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
返回排行榜