Component Building Challenges
Practice building real React components from scratch.
Challenge 1: Toggle Switch
Build a reusable toggle switch component.
Requirements:
- Accept
checkedandonChangeprops - Accessible (keyboard navigation, aria labels)
- Customizable colors via props
Solution:
jsx
1function Toggle({ checked, onChange, label }) {
2 const handleKeyDown = (e) => {
3 if (e.key === 'Enter' || e.key === ' ') {
4 e.preventDefault()
5 onChange(!checked)
6 }
7 }
8
9 return (
10 <label className="flex items-center gap-3 cursor-pointer">
11 <div
12 role="switch"
13 aria-checked={checked}
14 tabIndex={0}
15 onClick={() => onChange(!checked)}
16 onKeyDown={handleKeyDown}
17 className={`
18 w-12 h-6 rounded-full transition-colors relative
19 ${checked ? 'bg-blue-500' : 'bg-gray-300'}
20 `}
21 >
22 <div
23 className={`
24 w-5 h-5 bg-white rounded-full absolute top-0.5
25 transition-transform shadow
26 ${checked ? 'translate-x-6' : 'translate-x-0.5'}
27 `}
28 />
29 </div>
30 {label && <span>{label}</span>}
31 </label>
32 )
33}
34
35// Usage
36function App() {
37 const [enabled, setEnabled] = useState(false)
38 return <Toggle checked={enabled} onChange={setEnabled} label="Notifications" />
39}Challenge 2: Accordion
Build an accordion that shows/hides content.
Requirements:
- Only one section open at a time
- Smooth animation
- Keyboard accessible
Solution:
jsx
1function Accordion({ items }) {
2 const [openIndex, setOpenIndex] = useState(null)
3
4 const toggle = (index) => {
5 setOpenIndex(openIndex === index ? null : index)
6 }
7
8 return (
9 <div className="border rounded-lg divide-y">
10 {items.map((item, index) => (
11 <AccordionItem
12 key={index}
13 title={item.title}
14 content={item.content}
15 isOpen={openIndex === index}
16 onToggle={() => toggle(index)}
17 />
18 ))}
19 </div>
20 )
21}
22
23function AccordionItem({ title, content, isOpen, onToggle }) {
24 const contentRef = useRef(null)
25 const [height, setHeight] = useState(0)
26
27 useEffect(() => {
28 if (contentRef.current) {
29 setHeight(isOpen ? contentRef.current.scrollHeight : 0)
30 }
31 }, [isOpen])
32
33 return (
34 <div>
35 <button
36 onClick={onToggle}
37 className="w-full px-4 py-3 flex justify-between items-center hover:bg-gray-50"
38 aria-expanded={isOpen}
39 >
40 <span className="font-medium">{title}</span>
41 <span className={`transition-transform ${isOpen ? 'rotate-180' : ''}`}>
42 ▼
43 </span>
44 </button>
45
46 <div
47 style={{ height }}
48 className="overflow-hidden transition-all duration-300"
49 >
50 <div ref={contentRef} className="px-4 py-3 text-gray-600">
51 {content}
52 </div>
53 </div>
54 </div>
55 )
56}
57
58// Usage
59const items = [
60 { title: 'Section 1', content: 'Content for section 1' },
61 { title: 'Section 2', content: 'Content for section 2' },
62 { title: 'Section 3', content: 'Content for section 3' },
63]
64
65<Accordion items={items} />Challenge 3: Star Rating
Build an interactive star rating component.
Requirements:
- Clickable stars to set rating
- Hover preview
- Read-only mode
- Half-star support (bonus)
Solution:
jsx
1function StarRating({ value, onChange, max = 5, readOnly = false }) {
2 const [hoverValue, setHoverValue] = useState(null)
3
4 const displayValue = hoverValue !== null ? hoverValue : value
5
6 return (
7 <div
8 className="flex gap-1"
9 onMouseLeave={() => !readOnly && setHoverValue(null)}
10 >
11 {Array.from({ length: max }, (_, i) => (
12 <Star
13 key={i}
14 filled={i < displayValue}
15 onClick={() => !readOnly && onChange(i + 1)}
16 onMouseEnter={() => !readOnly && setHoverValue(i + 1)}
17 disabled={readOnly}
18 />
19 ))}
20 </div>
21 )
22}
23
24function Star({ filled, onClick, onMouseEnter, disabled }) {
25 return (
26 <button
27 type="button"
28 onClick={onClick}
29 onMouseEnter={onMouseEnter}
30 disabled={disabled}
31 className={`
32 text-2xl transition-colors
33 ${filled ? 'text-yellow-400' : 'text-gray-300'}
34 ${!disabled && 'hover:scale-110 cursor-pointer'}
35 `}
36 >
37 ★
38 </button>
39 )
40}
41
42// Usage
43function App() {
44 const [rating, setRating] = useState(3)
45
46 return (
47 <div>
48 <StarRating value={rating} onChange={setRating} />
49 <p>Rating: {rating}</p>
50
51 <StarRating value={4} readOnly />
52 </div>
53 )
54}Challenge 4: Tabs Component
Build a tabs component with compound component pattern.
Solution:
jsx
1const TabsContext = createContext()
2
3function Tabs({ children, defaultValue }) {
4 const [activeTab, setActiveTab] = useState(defaultValue)
5
6 return (
7 <TabsContext.Provider value={{ activeTab, setActiveTab }}>
8 <div className="w-full">{children}</div>
9 </TabsContext.Provider>
10 )
11}
12
13function TabList({ children }) {
14 return (
15 <div className="flex border-b" role="tablist">
16 {children}
17 </div>
18 )
19}
20
21function Tab({ value, children }) {
22 const { activeTab, setActiveTab } = useContext(TabsContext)
23 const isActive = activeTab === value
24
25 return (
26 <button
27 role="tab"
28 aria-selected={isActive}
29 onClick={() => setActiveTab(value)}
30 className={`
31 px-4 py-2 font-medium transition-colors
32 ${isActive
33 ? 'border-b-2 border-blue-500 text-blue-600'
34 : 'text-gray-500 hover:text-gray-700'}
35 `}
36 >
37 {children}
38 </button>
39 )
40}
41
42function TabPanel({ value, children }) {
43 const { activeTab } = useContext(TabsContext)
44
45 if (activeTab !== value) return null
46
47 return (
48 <div role="tabpanel" className="p-4">
49 {children}
50 </div>
51 )
52}
53
54// Attach sub-components
55Tabs.List = TabList
56Tabs.Tab = Tab
57Tabs.Panel = TabPanel
58
59// Usage
60<Tabs defaultValue="tab1">
61 <Tabs.List>
62 <Tabs.Tab value="tab1">Tab 1</Tabs.Tab>
63 <Tabs.Tab value="tab2">Tab 2</Tabs.Tab>
64 <Tabs.Tab value="tab3">Tab 3</Tabs.Tab>
65 </Tabs.List>
66
67 <Tabs.Panel value="tab1">Content 1</Tabs.Panel>
68 <Tabs.Panel value="tab2">Content 2</Tabs.Panel>
69 <Tabs.Panel value="tab3">Content 3</Tabs.Panel>
70</Tabs>Challenge 5: Infinite Scroll
Build a list with infinite scroll loading.
Solution:
jsx
1function useInfiniteScroll(callback, options = {}) {
2 const observer = useRef()
3 const { threshold = 0.1 } = options
4
5 const lastElementRef = useCallback(node => {
6 if (observer.current) observer.current.disconnect()
7
8 observer.current = new IntersectionObserver(entries => {
9 if (entries[0].isIntersecting) {
10 callback()
11 }
12 }, { threshold })
13
14 if (node) observer.current.observe(node)
15 }, [callback, threshold])
16
17 return lastElementRef
18}
19
20function InfiniteList() {
21 const [items, setItems] = useState([])
22 const [page, setPage] = useState(1)
23 const [loading, setLoading] = useState(false)
24 const [hasMore, setHasMore] = useState(true)
25
26 const loadMore = async () => {
27 if (loading || !hasMore) return
28
29 setLoading(true)
30 const newItems = await fetchItems(page)
31
32 setItems(prev => [...prev, ...newItems])
33 setPage(prev => prev + 1)
34 setHasMore(newItems.length > 0)
35 setLoading(false)
36 }
37
38 const lastItemRef = useInfiniteScroll(loadMore)
39
40 return (
41 <div className="space-y-4">
42 {items.map((item, index) => (
43 <div
44 key={item.id}
45 ref={index === items.length - 1 ? lastItemRef : null}
46 className="p-4 border rounded"
47 >
48 {item.title}
49 </div>
50 ))}
51
52 {loading && <div className="text-center py-4">Loading...</div>}
53 {!hasMore && <div className="text-center py-4">No more items</div>}
54 </div>
55 )
56}Practice these patterns to become a better React developer!
