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 wrapperimport { 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 contestoexport function useAccordionContext() { const context = useContext(AccordionContext) if (!context) { throw new Error('Accordion components must be wrapped by <Accordion>') } return context}
export default AccordionComponenti Figli
I componenti figli accedono al contesto tramite il custom hook:
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 AccordionItemComponenti Specializzati
Si possono creare componenti più specializzati per maggiore configurabilità:
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.jsximport { 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 AccordionContentRaggruppamento con Proprietà di Funzione
Un pattern comune è raggruppare tutti i componenti correlati come proprietà del componente principale:
import AccordionItem from './AccordionItem'import AccordionTitle from './AccordionTitle'import AccordionContent from './AccordionContent'
function Accordion({ children, className }) { // ... logica del componente}
// Raggruppa i componenti correlatiAccordion.Item = AccordionItemAccordion.Title = AccordionTitleAccordion.Content = AccordionContent
export default AccordionUtilizzo:
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:
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 SearchableListUtilizzo con Render Props
Il componente che usa SearchableList passa una funzione che definisce come renderizzare ogni item:
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:
- Children come funzione:
childrenè una funzione che viene chiamata - Prop dedicata: una prop come
renderorenderItemviene 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
- Prima chiamata: viene creato un timer che aggiornerà lo state dopo 500ms
- Chiamate successive: se l’utente digita di nuovo prima che il timer scada, il timer precedente viene cancellato e ne viene creato uno nuovo
- 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 Propsfunction 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 = DataTableHeaderDataTable.Row = DataTableRowDataTable.Cell = DataTableCellRiepilogo
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.