
Compound Component Pattern en React JS: Simplificando la Composición y Reutilización de Componentes 2025

Juan Correa
Desarrollador de Software Senior
El compound component pattern es una solución para compartir un estado entre componentes de manera implícita, sin necesidad de pasar props directamente.
Si lo anterior te suena en chino, ¡no te preocupes! la primera vez siempre es confuso, pero aquí te lo explico facilito.
La composición de componentes es parte fundamental de React. Creamos componentes y los anidamos (componemos) para expresar la UI en términos declarativos.
Este patrón aprovecha al máximo la composición para que podamos implementar componentes muy flexibles que comparten un estado común pero haciendo que se comuniquen internamente sin necesidad de pasar props.
La manera de comunicar internamente a los componentes es por medio de React context, los cuales nos permiten crear un estado (contexto) y compartirlo con los componentes que deseemos.
Table of Contents
¿Qué es el Compound Component Pattern?
El Patrón de Componentes Compuestos en React es un patrón de diseño en React.js que, al ser combinado con React context, permite compartir estado entre componentes relacionados de manera implícita, sin tener que pasar props explícitamente a través de cada nivel del árbol de componentes.
¿Quieres dominar los Patrones de Diseño en React.js?
En mi ebook "Patrones avanzados en React.js", encontrarás más información sobre los patrones explicados en este post y contenido de alta calidad que te ayudará a llevar tus habilidades al siguiente nivel.
Descargar ebook gratisEjemplo de Compound component en vídeo
Puedes complementar con el vídeo que he preparado como parte del curso de Patrones de Diseño en React JS.
Ventajas de Compound Components Pattern
Las principales ventajas al usar este patrón son:
- Máxima flexibilidad en la estructura de un componente debido a que el patrón deja abierto al componente para su extensión y cerrado para su modificación (Principio de abierto - cerrado).
- Reduce la complejidad derivada al implementar otros patrones como render props (veremos este patrón en su capítulo correspondiente).
- División de responsabilidades al tener componentes dedicados a hacer una sola cosa y que la hagan bien.
Desventajas de Usar Compount Components Pattern
- Al usar React Context estamos limitando la capacidad de reuso de los componentes hijos por fuera del contexto en donde apliquemos este patrón.
- Si usas una capa de estado como Redux o Mobx considero que aun así puedes usar este patrón, pero tienes que adaptar tus componentes a que se conecten a dicha capa. Lo considero una “desventaja” porque la mayoría de los ejemplos en la web son con React context.
En qué casos aplicar Compound Pattern
Considera usar este patrón si:
- Tienes un componente que debido a los cambios naturales de un proyecto, necesitas que sea muy flexible.
- Como corolario del punto anterior, normalmente vas a aplicar este patrón como un refactor y no a priori, es decir, no desde un inicio a menos que sepas de antemano que necesitarás la máxima flexibilidad.
Debido a estos puntos, el ejemplo que haremos va a ser un componente normal y luego lo vamos a refactorizar.
Ejemplo de Compound Pattern
Vamos a crear una clásica lista de cosas por hacer (TODO list) sencilla y la iremos refactorizando hasta aplicar el Compound pattern.
Una TODO list suele componerse por:
- Un título.
- Una sección para ingresar nuevas cosas por hacer.
- El listado de las cosas por hacer en sí mismo.
- Una opción para marcar como realizadas las cosas por hacer en nuestra lista.
Veamos como es la primera versión de nuestra app.
const Todo = ({ title }) => {
const [listTodos, setListTodos] = useState({})
const [inputValue, setInputValue] = useState('')
const handleInputChange = ({ target: { value } }) => {
setInputValue(value)
}
const handleSubmit = (e) => {
e.preventDefault()
setListTodos({
...listTodos,
[inputValue]: { name: inputValue, isDone: false },
})
setInputValue('')
}
const toogleTodo = ({ target: { name } }) => {
setListTodos({
...listTodos,
[name]: {
...listTodos[name],
isDone: !listTodos[name].isDone,
},
})
}
const getTodoValues = () => Object.values(listTodos)
return (
<div className="todo">
<header>
<h2>{title}</h2>
</header>
<main className="todo-main">
<div>
<form onSubmit={handleSubmit}>
<label>
New todo:
<input type="text" value={inputValue} onChange={handleInputChange} />
</label>
<button type="submit">Add</button>
</form>
</div>
</main>
<footer>
{!getTodoValues().length && <p>No todo added yet.</p>}
<ul>
{getTodoValues().map(({ name, isDone }) => (
<li>
<label>
<input name={name} type="checkbox" checked={isDone} onChange={toogleTodo} />{' '}
<span className={isDone ? 'line-through' : ''}>{name}</span>
</label>
</li>
))}{' '}
</ul>
</footer>
</div>
)
}
El único prop que recibe nuestro componente Todo es un title, el resto de valores los conforma el mismo componente por lo que no es necesario pasar más props.
Esta es la manera en la que se usa el componente.
export default function App() {
return (
<div className="App">
<Todo title="My Todo" />
</div>
)
}
Refactorizando el Componente Todo a subcomponentes
Probablemente estés pensando que podríamos haber creado subcomponentes para que el componente Todo no sea tan grande y tenga tantas responsabilidades.
Lo anterior es absolutamente verdad, por lo que vamos a hacer ese refactor como un paso previo antes de comenzar a aplicar el patrón.
Componente para mostrar la cabecera.
const TodoTitle = ({ children }) => <header>{children}</header>
Componente encargado de mostrar el formulario para crear nuevas cosas por hacer:
const TodoForm = ({ onSubmit }) => {
const [inputValue, setInputValue] = useState('')
const handleInputChange = ({ target: { value } }) => {
setInputValue(value)
}
const handleSubmit = (e) => {
e.preventDefault()
onSubmit(inputValue)
setInputValue('')
}
return (
<form onSubmit={handleSubmit}>
<label>
New todo:
<input type="text" value={inputValue} onChange={handleInputChange} />
</label>
<button type="submit">Add</button>
</form>
)
}
Componente que va a mostrar el listado de las cosas por hacer:
const TodoList = ({ list, onChange }) => {
return (
<ul>
{list.map(({ name, isDone }) => (
<li>
<label>
<input name={name} type="checkbox" checked={isDone} onChange={onChange} />{' '}
<span className={isDone ? 'line-through' : ''}>{name}</span>
</label>
</li>
))}
</ul>
)
}
Y finalmente, el componente Todo queda como:
const Todo = ({ title }) => {
const [listTodos, setListTodos] = useState({})
const handleSubmit = (inputValue) => {
setListTodos({
...listTodos,
[inputValue]: { name: inputValue, isDone: false },
})
}
const toogleTodo = ({ target: { name } }) => {
setListTodos({
...listTodos,
[name]: {
...listTodos[name],
isDone: !listTodos[name].isDone,
},
})
}
const getTodoValues = () => Object.values(listTodos)
const todoListValues = getTodoValues()
return (
<div className="todo">
<TodoTitle>
<h2>{title}</h2>
</TodoTitle>
<main className="todo-main">
<TodoForm onSubmit={handleSubmit} />
</main>
<footer>
{!todoListValues.length && <p>No todo added yet.</p>}
<TodoList list={todoListValues} onChange={toogleTodo} />
</footer>
</div>
)
}
La manera de implementarlo sigue siendo la misma. Es decir, para usarlo basta con hacer:
export default function App() {
return (
<div className="App">
<Todo title="My Todo" />
</div>
)
}
Puedes ver y jugar con este código en este enlace: https://codesandbox.io/s/todo-example-mcl3i.
Problemas de la implementación actual
Hasta ahora todo luce un poco mejor. Sin embargo, el problema que veo aquí son tres:
- Elcomponente Todo,apesar de que internamente se compone por una estructura de header, main y footer, cuando vemos
<Todo title="My Todo" />no es explícito. Es decir, tenemos que leer y comprender el componenteTodopara poder saber que tiene esa estructura. En pocas palabras, su estructura no es explícita ni expresiva. - En este ejemplo estamos pasando el prop title a dos niveles abajo en el árbol de componentes. Es decir, en
Apppasamos el prop title aTodo, y deTodopasamos el prop title aTodoTitle. No es tan malo, pero consideremos que abre las puertas al caos para comenzar a pasar más props a través del árbol de componentes cuando la app evolucione. - Para cambiar la estructura o añadir nuevos elementos, el componente
Todotenderá a crecer más y más, tanto para añadir los nuevos elementos como para colocar lógica de renderizado como condicionales, props y estilos css. Este es el principal problema en mi opinión.
Aplicando el Compound Component Pattern
La manera de aplicar el patrón consiste en dos pasos:
- Abstraer el manejo del estado interno del componente.
- Usar un mecanismo para comunicar el estado de manera interna entre los componentes hijos.
Para abstraer el manejo del estado interno usaremos React Context.
import { createContext } from ‘react’;
const TodoContext = createContext({});
const { Provider } = TodoContext;
Definimos el valor del contexto inicial como un objeto vacío, y destructuramos Provider de TodoContext. Provider lo usaremos para habilitar el uso de este contexto en los componentes hijos como veremos a continuación.
const Todo = ({ children }) => {
const [listTodos, setListTodos] = useState({})
const handleSubmit = (inputValue) => {
setListTodos({
...listTodos,
[inputValue]: { name: inputValue, isDone: false },
})
}
const toogleTodo = ({ target: { name } }) => {
setListTodos({
...listTodos,
[name]: {
...listTodos[name],
isDone: !listTodos[name].isDone,
},
})
}
const getTodoValues = () => Object.values(listTodos)
const providerValue = {
getTodoValues,
toogleTodo,
handleSubmit,
}
return (
<div className="todo">
<Provider value={providerValue}>{children}</Provider>
</div>
)
}
Este ha sido un cambio muy radical debido a que el componente Todo ya no renderiza los subcomponentes que lo conforman, sino que se limita a actuar como un contenedor y renderiza lo que pasemos por children.
Podemos afirmar que se ha convertido en el componente con la lógica de cómo aplicar las reglas que deseamos independiente de la UI.
El valor de Provider consiste en un objeto con las funciones necesarias para que los subcomponentes puedan seguir trabajando internamente como veremos a continuación.
const TodoForm = () => {
const [inputValue, setInputValue] = useState('')
const { handleSubmit } = useContext(TodoContext)
const handleInputChange = ({ target: { value } }) => {
setInputValue(value)
}
const _handleSubmit = (e) => {
e.preventDefault()
handleSubmit(inputValue)
setInputValue('')
}
return (
<form onSubmit={_handleSubmit}>
<label>
New todo:
<input type="text" value={inputValue} onChange={handleInputChange} />
</label>
<button type="submit">Add</button>
</form>
)
}
Ahora TodoForm no recibe nada por props. Estamos usando el hook useContext para poder extraer los valores que necesita de manera interna.
Lo mismo haremos con el componente TodoList.
const TodoList = () => {
const { getTodoValues, toogleTodo } = useContext(TodoContext)
const list = getTodoValues()
if (!list.length) {
return <p>No todo added yet.</p>
}
return (
<ul>
{list.map(({ name, isDone }) => (
<li>
<label>
<input name={name} type="checkbox" checked={isDone} onChange={toogleTodo} />
<span className={isDone ? 'line-through' : ''}>{name}</span>
</label>
</li>
))}
</ul>
)
}
Con estos cambios ya podemos ver “la magia” al momento de implementar el componente Todo. Lo que haremos es pasar los subcomponentes como hijos.
export default function App() {
return (
<div className="App">
<Todo>
<TodoTitle>
<h2>My Todo with Compound Pattern</h2>
</TodoTitle>
<main className="todo-main">
<TodoForm />
</main>
<footer>
<TodoList />
</footer>
</Todo>
</div>
)
}
Ahora es mucho más explícito y expresivo la manera en la que podemos usar el componente Todo. Esto es un gran cambio, ¿No es así?
Para ver el código fuente, entra a este enlace: https://codesandbox.io/s/todo-example-compound-pattern-jygbm?file=/src/App.js.
¿No es más complejo el código?
Tal vez te estés preguntando lo siguiente:
- “Muy bonito y todo Juan, pero ¿Esto no es un ejemplo de sobre ingeniería o complejidad innecesaria? ¿Para qué hacer todo esto?”
La respuesta a este posible pensamiento es: El hecho de tener una estructura más explícita ya es una ventaja, pero si no vas a aprovechar el resto de ventajas de este patrón, estoy de acuerdo que es agregar complejidad innecesaria.
Si estás creando una app que es volátil y necesitas diseñar tu estructura de componentes que sea altamente flexible y abierta al cambio sin tener que modificar tus componentes base, sin duda es un patrón que necesitas considerar.
Para ilustrar la idea anterior, consideremos que ahora necesitamos crear diferentes Todo y cada uno con sus propias características, tanto en estilos como en estructura, y si nos ponemos más apegados a lo que suele ser una app de tamaño mediano, con la lógica de permisos por roles de usuario.
¿Te imaginas cómo podría crecer la complejidad del componente Todo original? Definir una estructura diferente, como por ejemplo, mostrar la lista de cosas por hacer y después el formulario en unos casos y en otros, viceversa.
O agregar nuevos elementos como un input para hacer una búsqueda y otro elemento para filtrar entre cosas por hacer terminadas y por terminar.
Pues bien, con este patrón hemos dejado nuestro componente Todo totalmente abierto a todos estos tipos de cambios sin incrementar la complejidad ni dejar deuda técnica.
De hecho, podemos afirmar que estamos aplicando el Open - Close Principle (Principio de Abierto - Cerrado), debido a que estamos dejando nuestro componente abierto a su extensión y cerrado a su modificación.
Para agregar un input de filtrado, basta con que creemos el nuevo componente y lo conectemos al TodoContext.
En el componente base Todo, creamos una nueva función con su lógica correspondiente para hacer el filtrado y la pasamos como valor al provider junto a los otros valores actuales.
Por último, otra variante opcional al aplicar este patrón es asignar los subcomponentes como propiedades del componente Todo (o el contenedor). Veamos un ejemplo de esto último.
Todo.Title = TodoTitle
Todo.Form = TodoForm
Todo.List = TodoList
export default function App() {
return (
<div className="App">
<Todo>
<Todo.Title>
<h2>My Todo with Compound Pattern</h2>
</Todo.Title>
<main className="todo-main">
<Todo.Form />
</main>
<footer>
<Todo.List />
</footer>
</Todo>
</div>
)
}
Vas a ver mucho esta variante de implementación en librerías de UI, por ejemplo, en MaterialUI.
Cheat Sheet de Compound Component Pattern
Aquí tienes un resumen de los pasos para implementar el patrón de Componentes Compuestos en React:
- Crea tu Contexto:
const ArticleContext = React.createContext()
- Crea Componentes que Consuman el Contexto: Estos componentes accederán al estado y funciones compartidos a través del contexto.
function Title() {
const { title } = React.useContext(ArticleContext)
return <h1>{title}</h1>
}
function Content() {
const { content } = React.useContext(ArticleContext)
return <p>{content}</p>
}
- Combina los Componentes en un Componente Padre con Proveedor de Contexto: Este componente actuará como el componente compuesto, proporcionando el estado y funciones a sus hijos.
function Article(props) {
return (
<ArticleContext.Provider value={props}>
<div>
<Title />
<Content />
</div>
</ArticleContext.Provider>
)
}
- Usa tu Componente Compuesto con Contexto:
<Article title="Título del Artículo" content="Contenido del artículo..." />
Conclusión
El patrón de Componentes Compuestos, en combinación con el Contexto de React, es una herramienta poderosa que facilita el intercambio de estado y funciones entre componentes relacionados, creando aplicaciones más limpias y mantenibles.
Si te ha gustado este análisis sobre el patrón de Componentes Compuestos con Contexto, te invito a explorar los otros patrones avanzados en React JS que he analizado en la guía principal.
Y recuerda, si quieres llevar tus habilidades al siguiente nivel, no te pierdas el curso completo en el Canal de YouTube de Developero.