Un patrón de diseño muy útil en React para crear componentes reutilizables es el de componentes compuestos.
¿¿Y en que consisten los componentes compuestos??
Este patrón consiste en descomponer un componente de React en varios componentes diseñados para usarse en conjunto y armar el componente compuesto como nos de la gana otorgando una mayor flexibilidad a quién lo implementa.
Nada mejor para entender este concepto que con un ejemplo.
Supongamos que tenemos el requerimiento de crear un pequeño teclado con 6 letras para introducir un texto y mostrarlo en pantalla (Super útil, ya sé, pero nos sirve para nuestro ejemplo). Hagamos algo sencillo para darle forma a la idea:
import { useState } from "react";
const KeyboardButton = ({ char, addChar }) => {
return (
<button onClick={() => addChar(char)}>{ char }</button>
);
};
const KeyboardWidget = () => {
const [text, setText] = useState("");
const addChar = (char) => {
setText(currentText => setText(currentText + char))
};
return (
<div style={{ textAlign: "center" }}>
<h3>{ text }</h3>
<KeyboardButton char="q" addChar={addChar} />
<KeyboardButton char="w" addChar={addChar} />
<KeyboardButton char="e" addChar={addChar} />
<KeyboardButton char="r" addChar={addChar} />
<KeyboardButton char="t" addChar={addChar} />
<KeyboardButton char="y" addChar={addChar} />
</div>
);
};
export default KeyboardWidget;¡Listo! Así de sencillo y nuestro teclado se debe ver así:

Tras desplegar nuestro componente se vuelve un éxito y es utilizado por varios equipos, sin embargo, como suele pasar, un equipo nos pide un requerimiento extra: Necesitan que el teclado funcione con números en vez de letras.
¿Cómo podemos hacer?
Sencillo, agregamos una prop como digits={true} o algo parecido. Sencillo, ¿Correcto?
Pero otro equipo necesita que el texto se muestre debajo de los botones de tecla en vez de arriba.
Otro equipo más requiere que incluyamos letras mayúsculas y carácteres especiales pero los demás equipos no requieren esto.
Para estar satisfaciendo todos estos requerimientos, si utilizamos props para configurar las diferentes opciones nuestro componente se puede volver muy complejo además de que vamos a estar necesitando darle mantenimiento constante añadiendo flujos para cada requerimiento específico.
Aquí es donde los componentes compuestos vienen al rescate.
Vamos a utilizar este patrón para descomponer nuestro widget de teclado en varios componentes y cada equipo se va a encargar de implementarlos como lo requieran.
¿Qué cómo hacemos esto?
Bueno, antes que nada vamos a mover la lógica y el estado de nuestro widget a una API que los componentes que vamos a crear puedan compartir. Para ello vamos a hacer uso del poder de los hooks y el context de React. Así quedaría:
import { createContext, useState, useCallback, useContext } from "react";
const keyboardContext = createContext();
const KeyboardProvider = ({ children }) => {
const [text, setText] = useState("");
const addChar = useCallback(
(char) => setText(currentText => currentText + char),
[]
);
return (
<keyboardContext.Provider value={{ text, addChar }} >
{ children }
</keyboardContext.Provider>
);
};
const useKeyboard = () => {
const context = useContext(keyboardContext);
if (!context) {
throw new Error("No se encontró el KeyboardProvider");
}
return context;
};
export {
KeyboardProvider,
useKeyboard,
};Veamos que está pasando aquí:
Primero que nada, hacemos uso del context de React para crear un provider que compartirá el estado y la lógica entre los componentes de nuestro widget.
Creamos un hook personalizado que simplemente hace uso de este context para utilizar esta lógica. Esto es lo mismo que usar useContext directamente pero lo hacemos de esta manera para simplificar el código y hacerlo más entendible.
Bien, ahora que resolvimos el como compartir la lógica de nuestro widget entre los componentes, procedamos a crear estos componentes, que básicamente serían dos.
Un botón de tecla genérico:
import { useKeyboard } from "./KeyboardApi";
const KeyboardButton = ({ char }) => {
const { addChar } = useKeyboard();
return (
<button onClick={() => addChar(char)}>{ char }</button>
);
};
export default KeyboardButton;Y el componente que muestra el texto introducido:
import { useKeyboard } from "./KeyboardApi";
const KeyboardButton = ({ char }) => {
const { addChar } = useKeyboard();
return (
<button onClick={() => addChar(char)}>{ char }</button>
);
};
export default KeyboardButton;El código es bastante sencillo, lo único a resaltar es como hacemos uso de nuestro useKeyboard hook para interactuar con el estado del widget, ¡Ese es el corazón de nuestro componente compuesto!
Para que esto funcione, necesitaremos hacer uso del Provider que creamos en un componente padre de los botones y el texto a mostrar. Haremos esto en el componente principal que quedaría como a continuación:
import { KeyboardProvider } from "./KeyboardApi";
import KeyboardText from "./KeyboardText";
import KeyboardButton from "./KeyboardButton";
const KeyboardWidget = ({ children }) => {
return (
<KeyboardProvider>
<div style={{ textAlign: "center" }}>
{ children }
</div>
</KeyboardProvider>
);
}
KeyboardWidget.Text = KeyboardText;
KeyboardWidget.Button = KeyboardButton;
export default KeyboardWidget;Así de sencillo.
A resaltar esas líneas al final donde asignamos los componentes de texto y botón a una propiedad del Widget principal (algo que nos permite Javascript por su particularidad de manejar todo como un objeto), esto para poder usar todos los componentes sólo importando el Widget principal.
Para que quede más claro veamos una implementación de ejemplo:
import KeyboardWidget from "./components/KeyboardWidget";
const App = () => {
return (
<KeyboardWidget>
<KeyboardWidget.Text />
<KeyboardWidget.Button char="q" />
<KeyboardWidget.Button char="w" />
<KeyboardWidget.Button char="e" />
<KeyboardWidget.Button char="r" />
<KeyboardWidget.Button char="t" />
<KeyboardWidget.Button char="y" />
</KeyboardWidget>
);
}
export default App;
Así de sencilla y flexible sería la implementación.
Ahora cada equipo puede importar nuestro Widget y armarlo como quiera, cambiando los botones, poniendo el texto en otro lugar incluso intercalando algunos otros componentes para organizar o separar.
Pues bien, este es un ejemplo muy sencillo de como implementar un componente compuesto pero espero tu imaginación ya esté volando y te des cuenta del potencial que tiene este patrón para requerimientos más complejos.
Cualquier duda no dudes en dejarme un comentario. Saludos