Skip
Arish's avatar

38. Component Building Challenges


Component Building Challenges

Practice building real React components from scratch.


Challenge 1: Toggle Switch

Build a reusable toggle switch component.

Requirements:

  • Accept checked and onChange props
  • 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' : ''}`}>
4243        </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    >
3738    </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!