Pattern Avanzati: Compound Components, Render Props e Debouncing

20 gennaio 2026
7 min di lettura

Introduzione

I pattern avanzati in React permettono di costruire componenti più flessibili, riutilizzabili e performanti. I Compound Components permettono di creare componenti che funzionano insieme per offrire maggiore configurabilità. Il Render Props pattern separa la logica dal rendering, permettendo di riutilizzare la logica con rendering diversi. Il debouncing ottimizza le performance ritardando l’esecuzione di operazioni costose. Questo articolo esplora come funzionano questi pattern e come implementarli.

Compound Components Pattern

I Compound Components sono componenti React che non funzionano da soli, ma solo insieme. L’esempio più comune sono gli elementi HTML select e option: select da solo non è utile, così come option da solo, ma insieme creano un dropdown funzionale.

Implementazione Base

Per implementare Compound Components, si crea un componente wrapper che gestisce lo stato condiviso tramite Context API, e componenti figli che accedono a quel contesto:

// Accordion.jsx - Componente wrapper
import { createContext, useState } from 'react'
const AccordionContext = createContext()
function Accordion({ children, className }) {
const [openItemId, setOpenItemId] = useState(null)
const toggleItem = (id) => {
setOpenItemId((prevId) => {
if (prevId === id) {
return null // Chiudi se già aperto
}
return id // Apri nuovo item
})
}
const contextValue = {
openItemId,
toggleItem
}
return (
<AccordionContext.Provider value={contextValue}>
<ul className={className}>
{children}
</ul>
</AccordionContext.Provider>
)
}
// Hook custom per accedere al contesto
export function useAccordionContext() {
const context = useContext(AccordionContext)
if (!context) {
throw new Error('Accordion components must be wrapped by <Accordion>')
}
return context
}
export default Accordion

Componenti Figli

I componenti figli accedono al contesto tramite il custom hook:

AccordionItem.jsx
import { createContext, useContext } from 'react'
import { useAccordionContext } from './Accordion'
const AccordionItemContext = createContext()
function AccordionItem({ id, children, className }) {
return (
<AccordionItemContext.Provider value={id}>
<li className={className}>
{children}
</li>
</AccordionItemContext.Provider>
)
}
function useAccordionItemContext() {
const context = useContext(AccordionItemContext)
if (!context) {
throw new Error('AccordionItem components must be wrapped by <Accordion.Item>')
}
return context
}
export default AccordionItem

Componenti Specializzati

Si possono creare componenti più specializzati per maggiore configurabilità:

AccordionTitle.jsx
import { useAccordionContext } from './Accordion'
import { useAccordionItemContext } from './AccordionItem'
function AccordionTitle({ children, className }) {
const { toggleItem } = useAccordionContext()
const id = useAccordionItemContext()
const handleClick = () => {
toggleItem(id)
}
return (
<h3 className={className} onClick={handleClick}>
{children}
</h3>
)
}
export default AccordionTitle
// AccordionContent.jsx
import { useAccordionContext } from './Accordion'
import { useAccordionItemContext } from './AccordionItem'
function AccordionContent({ children, className }) {
const { openItemId } = useAccordionContext()
const id = useAccordionItemContext()
const isOpen = openItemId === id
return (
<div className={`${className} ${isOpen ? 'open' : 'close'}`}>
{children}
</div>
)
}
export default AccordionContent

Raggruppamento con Proprietà di Funzione

Un pattern comune è raggruppare tutti i componenti correlati come proprietà del componente principale:

Accordion.jsx
import AccordionItem from './AccordionItem'
import AccordionTitle from './AccordionTitle'
import AccordionContent from './AccordionContent'
function Accordion({ children, className }) {
// ... logica del componente
}
// Raggruppa i componenti correlati
Accordion.Item = AccordionItem
Accordion.Title = AccordionTitle
Accordion.Content = AccordionContent
export default Accordion

Utilizzo:

App.jsx
import Accordion from './components/Accordion'
function App() {
return (
<Accordion className="accordion">
<Accordion.Item id="experience" className="accordion-item">
<Accordion.Title className="accordion-item-title">
We got 20 years of experience
</Accordion.Title>
<Accordion.Content className="accordion-item-content">
<article>
<p>You can't go wrong with us...</p>
</article>
</Accordion.Content>
</Accordion.Item>
</Accordion>
)
}
Vantaggi dei Compound Components
  • Alta configurabilità: ogni parte del componente può essere personalizzata
  • Separazione delle responsabilità: ogni componente gestisce una parte specifica
  • Riutilizzabilità: i componenti possono essere combinati in modi diversi
  • Type safety: con TypeScript, si possono definire tipi che garantiscono l’uso corretto

Render Props Pattern

Il Render Props pattern consiste nel passare una funzione come prop (spesso children) che restituisce JSX. Questo permette di separare la logica dal rendering, rendendo i componenti più flessibili.

Implementazione Base

Un componente che gestisce la logica ma delega il rendering:

SearchableList.jsx
import { useState, useRef } from 'react'
function SearchableList({ items, children, itemKey }) {
const [searchTerm, setSearchTerm] = useState('')
const lastChange = useRef(null)
// Logica di ricerca
const searchResults = items.filter((item) => {
const itemString = JSON.stringify(item).toLowerCase()
return itemString.includes(searchTerm.toLowerCase())
})
// Debouncing per ottimizzare le performance
const handleChange = (event) => {
// Cancella il timer precedente se esiste
if (lastChange.current) {
clearTimeout(lastChange.current)
}
// Imposta un nuovo timer
lastChange.current = setTimeout(() => {
setSearchTerm(event.target.value)
lastChange.current = null
}, 500)
}
return (
<div className="searchable-list">
<input
type="search"
placeholder="Search"
onChange={handleChange}
/>
<ul>
{searchResults.map((item) => (
<li key={itemKey ? itemKey(item) : item}>
{children(item)}
</li>
))}
</ul>
</div>
)
}
export default SearchableList

Utilizzo con Render Props

Il componente che usa SearchableList passa una funzione che definisce come renderizzare ogni item:

App.jsx
import SearchableList from './components/SearchableList'
import Place from './components/Place'
function App() {
const places = [
{ id: 1, name: 'Safari in Africa', location: 'Kenya' },
{ id: 2, name: 'Beach Paradise', location: 'Maldives' }
]
const simpleItems = ['item one', 'item two']
return (
<div>
{/* Render props per oggetti complessi */}
<SearchableList
items={places}
itemKey={(item) => item.id}
>
{(item) => <Place item={item} />}
</SearchableList>
{/* Render props per dati semplici */}
<SearchableList
items={simpleItems}
itemKey={(item) => item}
>
{(item) => <span>{item}</span>}
</SearchableList>
</div>
)
}

Vantaggi del Render Props Pattern

  • Separazione logica/rendering: la logica di ricerca è nel componente, il rendering è definito dall’esterno
  • Flessibilità: lo stesso componente può essere usato con rendering completamente diversi
  • Riutilizzabilità: la logica può essere riutilizzata senza duplicare codice
Render Props vs Children Function

Il pattern Render Props può essere implementato in due modi:

  1. Children come funzione: children è una funzione che viene chiamata
  2. Prop dedicata: una prop come render o renderItem viene passata come funzione

Entrambi gli approcci funzionano, ma usare children come funzione è più comune e intuitivo.

Debouncing

Il debouncing è una tecnica che ritarda l’esecuzione di una funzione fino a quando non è passato un certo tempo dall’ultima chiamata. È utile per ottimizzare operazioni costose come filtraggio, ricerca o chiamate API.

Implementazione con useRef e setTimeout

import { useState, useRef } from 'react'
function SearchableList({ items, children }) {
const [searchTerm, setSearchTerm] = useState('')
const lastChange = useRef(null)
const handleChange = (event) => {
const value = event.target.value
// Se c'è un timer in corso, cancellalo
if (lastChange.current) {
clearTimeout(lastChange.current)
}
// Imposta un nuovo timer
lastChange.current = setTimeout(() => {
setSearchTerm(value)
lastChange.current = null // Pulisci il ref quando il timer scade
}, 500) // Attendi 500ms dopo l'ultima digitazione
}
// ... resto del componente
}

Come Funziona il Debouncing

  1. Prima chiamata: viene creato un timer che aggiornerà lo state dopo 500ms
  2. Chiamate successive: se l’utente digita di nuovo prima che il timer scada, il timer precedente viene cancellato e ne viene creato uno nuovo
  3. Timer scade: solo quando l’utente smette di digitare per 500ms, lo state viene aggiornato

Questo significa che se l’utente digita “react” rapidamente:

  • Non ci sono aggiornamenti durante la digitazione
  • Solo dopo 500ms dall’ultimo carattere, viene eseguito l’aggiornamento con “react”

Vantaggi del Debouncing

  • Performance migliorate: riduce il numero di operazioni eseguite
  • Riduce chiamate API: utile quando ogni ricerca triggera una richiesta HTTP
  • Migliore UX: evita aggiornamenti troppo frequenti che possono causare lag
Debouncing vs Throttling

Debouncing ritarda l’esecuzione fino a quando non passa un certo tempo dall’ultima chiamata. Throttling limita l’esecuzione a una volta ogni X millisecondi, garantendo esecuzioni regolari.

  • Debouncing: “esegui solo se non ci sono state chiamate per X ms”
  • Throttling: “esegui al massimo una volta ogni X ms”

Per la ricerca, il debouncing è generalmente più appropriato perché vogliamo aspettare che l’utente finisca di digitare.

Pattern Combinati

Questi pattern possono essere combinati per creare componenti ancora più potenti:

// Componente che combina Compound Components e Render Props
function DataTable({ data, children }) {
const [sortBy, setSortBy] = useState(null)
const [filterTerm, setFilterTerm] = useState('')
// Logica di sorting e filtering
const processedData = useMemo(() => {
let result = [...data]
if (filterTerm) {
result = result.filter((item) =>
JSON.stringify(item).toLowerCase().includes(filterTerm.toLowerCase())
)
}
if (sortBy) {
result.sort((a, b) => {
// Logica di sorting
})
}
return result
}, [data, filterTerm, sortBy])
return (
<DataTableContext.Provider value={{ sortBy, setSortBy, filterTerm, setFilterTerm }}>
{children(processedData)}
</DataTableContext.Provider>
)
}
DataTable.Header = DataTableHeader
DataTable.Row = DataTableRow
DataTable.Cell = DataTableCell

I pattern avanzati in React permettono di costruire componenti più flessibili e performanti. I Compound Components permettono di creare componenti configurabili che lavorano insieme tramite Context API. Il Render Props pattern separa la logica dal rendering, permettendo di riutilizzare la logica con rendering diversi. Il debouncing ottimizza le performance ritardando operazioni costose fino a quando l’utente non smette di interagire. Questi pattern possono essere combinati per creare soluzioni ancora più potenti e riutilizzabili.

Continua la lettura

Leggi il prossimo capitolo: "Testing di Applicazioni React"

Continua a leggere