La composición es un principio de diseño fundamental en React.js. Comprenderla a profundidad te ayudará a diseñar mucho mejor tus componentes. Y puedes ponerla en práctica usando distintos patrones de render.
Los componentes son la base de todo en React.js. Su característica principal es que pueden integrarse entre sí para cambiar el funcionamiento o resultado visual de la aplicación sin dificultades. Esto es lo que llamamos composición.
React es muy flexible. No nos impone una forma super estricta de trabajar. Y por esto mismo podemos llegar a traicionar su filosofía. Afortunadamente, estudiando sus principios de diseño podemos encontrar una mejor forma de diseñar nuestros componentes.
Estos principios, incluyendo el de composición, nos invitan a desarrollar componentes de fácil acoplamiento. Es decir, componentes que sean fáciles de mover, reutilizar e incluso eliminar sin traer consecuencias catastróficas a la app (como si fueran fichas de lego).
Gracias a la composición son posibles metodologías como Atomic Design, donde diseñamos partiendo de los elementos más pequeños en una app, integrándolos entre sí para formar componentes más grandes que finalmente se convierten en plantillas y páginas muy flexibles.
En React.js existen muchos patrones de render que facilitan este proceso de composición. El más obvio son las props, ya que nos permiten cambiar ciertos detalles de nuestros componentes cada nueva vez que los utilizamos.
functionComment({ likes, newLike }) {
return (
<article><p>Este comentario tiene {likes} likes</p><buttononClick={newLike}>¡Nuevo like!</button></article>
);
}
functionApp() {
const [likes1, setLikes1] = React.useState(0);
const [likes2, setLikes2] = React.useState(5);
const [likes3, setLikes3] = React.useState(25);
return (
<>
<Comment likes={likes1} newLike={setLikes1} />
<Comment likes={likes2} newLike={setLikes2} />
<Comment likes={likes3} newLike={setLikes3} />
</>
);
}
También existe la prop children. Es muy especial porque le permite a nuestros componentes desentenderse de cuál será su contenido interno, delegando esa responsabilidad a cualquier otro componente que nos necesite en el futuro.
functionComment({ likes, newLike, children }) {
return (
<article><p>Este comentario tiene {likes} likes</p><buttononClick={newLike}>¡Nuevo like!</button>
{children}
</article>
);
}
functionApp() {
const [likes1, setLikes1] = React.useState(0);
const [likes2, setLikes2] = React.useState(5);
const [likes3, setLikes3] = React.useState(25);
return (
<><Commentlikes={likes1}newLike={setLikes1}><p>Definitivamente no es un aguacate</p></Post><Commentlikes={likes2}newLike={setLikes2}><span>Parece un aguacate, pero no lo es</span></Post><Commentlikes={likes2}newLike={setLikes2}><h2>Esto no es un aguacate y te explico por qué</h2><p>Los aguacates son verdecitos, pero esto es azul marino, así que no es un aguacate</p><p>Like si quieres más tutoriales</p></Post></>
);
}
Incluso podemos replicar este funcionamiento para definir el contenido interno de nuestros componentes, pero no necesariamente con la prop children, sino con cualquier otra prop.
Este patrón es al que llamamos render props y render functions.
function Comment({ likesSection, contentSection }) {
const [likes, newLike] = React.useState(0);
return (
<article><divclassName="LikesSection">
{likesSection({ likes, newLike })}
</div><divclassName="ContentSection">
{contentSection()}
</div></article>
);
}
function App() {
return (
<><CommentlikesSection={({likes }) => (
<p>Este comentario tiene {likes} likes</p>
)}
contentSection={() => (
<p>Definitivamente no es un aguacate</p>
)}
/>
<CommentlikesSection={({newLike }) => (
<buttononClick={newLike}>¡Nuevo like!</button>
)}
contentSection={() => (
<span>Parece un aguacate, pero no lo es</span>
)}
/>
<CommentlikesSection={({likes, newLike }) => (
<><p>Comentario con {likes} likes</p><buttononClick={newLike}>Dar like</button></>
)}
contentSection={() => (
<><h2>Esto no es un aguacate y te explico por qué</h2><p>Los aguacates son verdecitos, pero esto es azul marino, así que no es un aguacate</p><p>Like si quieres más tutoriales</p></>
)}
/>
</>
);
}
Y también existen otros patrones como los High Order Components (HOCs) que nos permiten, entre otras cosas, inyectarle props a nuestros componentes sin necesidad de enviarlas desde los componentes que los llaman.
function Comment({ likes, newLike, likesSection, contentSection }) {
return (
<article><divclassName="LikesSection">
{likesSection({ likes, newLike })}
</div><divclassName="ContentSection">
{contentSection()}
</div></article>
);
}
function withLikes(WrappedComponent) {
return function WrappedComponentWithLikes(props) {
const [likes, newLike] = React.useState(0);
return (
<><WrappedComponent
{...props}
likes={likes}newLike={newLike}
/></>
);
}
}
const CommentWithLikes = withLikes(Comment);
function App() {
return (
<><CommentWithLikeslikesSection={({likes, newLike }) => (
<><p>Comentario con {likes} likes</p><buttononClick={newLike}>Dar like</button></>
)}
contentSection={() => (
<><h2>Esto no es un aguacate y te explico por qué</h2><p>Los aguacates son verdecitos, pero esto es azul marino, así que no es un aguacate</p><p>Like si quieres más tutoriales</p></>
)}
/>
</>
);
}
¿Qué tan fácil o difícil es leer tu código? ¿Puedes entender la estructura de tu app a simple vista?
Esto en desarrollo de software lo llamamos legibilidad. Y mantener una composición saludable es fundamental cuando trabajamos la legibilidad de nuestros componentes.
Es común encontrar aplicaciones donde el componente principal (digamos que es App.js) solo llama a sus “hijos” directos. A su vez, estos hijos directos solo llaman a sus propios hijos directos. Y así sucesivamente en todos los niveles.
No está mal. Funciona. Pero no nos permite ver de forma fácil cuál es la estructura completa de la app.
También podemos encontrarnos problemas como el prop drilling: la necesidad de enviar props y más props a muchísimos componentes que realmente no las van a usar, pero que terminan funcionando como puente para que las props lleguen a alguno de sus componentes hijos que sí las requieren.
Problemas como este los resolvemos normalmente con estados globales usando React Context o incluso herramientas como Redux.
Tampoco está mal. Pero cada nueva herramienta y cada nuevo estado global inevitablemente le agrega complejidad a nuestra aplicación. No siempre vale la pena.
Veamos un ejemplo (sin props, solo con la estructura de componentes):
functionApp() {
return (
<>
<LearningPathsList />
<ChallengesList />
</>
);
}
function LearningPathsList() {
return (
<section className="learningPaths-container">
{lps.map(lp => (
<LearningPath {...lp} />
))}
</section>
);
}
function ChallengesList() {
return (
<section className="challenges-container">
{challenges.map(challenge => (
<Challenge {...challenge} />
))}
</section>
);
}
function LearningPath() {
return (
<article className="learningPaths-item">
…
</article>
);
}
function Challenge() {
return (
<article className="challenges-item">
…
</article>
);
}
El componente App solo sabe que existen los componentes LearningPathsList y ChallengesList, pero no sabe qué habrá por dentro de estos componentes.
Imagina si además debemos enviarle props a cualquiera de estos componentes, pero ellos no las utilizan, sino que las envían a otros componentes (y quién sabe si ellos ahora sí usarán las props o volverán ser solo otro puente).
Es muy difícil identificar quién necesita la información que estamos comunicando. Tal vez incluso dejamos de usarla desde hace mucho, pero no nos dimos cuenta por estar “a ciegas” y con miedo a romper algún otro componente.
Todos estos problemas se resuelven aplicando composición de componentes. Todos los patrones que te conté pueden ayudarnos con sus distintas soluciones. Pero incluso algo tan sencillo como usar la prop children puede ahorrarnos esa complejidad muchas veces innecesaria.
Retomando el ejemplo, la diferencia será que nuestro componente App tiene mucha más responsabilidad, pero por lo mismo nos permite ver más allá en nuestra aplicación de un solo vistazo (y de paso también resolvería el problema de prop drilling sin estados globales):
functionApp() {
return (
<>
<LearningPathsList>
{lps.map(lp => (
<LearningPath {...lp} />
))}
</LearningPathsList>
<ChallengesList>
{challenges.map(challenge => (
<Challenge {...challenge} />
))}
</ChallengesList>
</>
);
}
function LearningPathsList({ children }) {
return (
<section className="learningPaths-container">
{children}
</section>
);
}
function ChallengesList({ children }) {
return (
<section className="challenges-container">
{children}
</section>
);
}
function LearningPath() {
return (
<article className="learningPaths-item">
…
</article>
);
}
function Challenge() {
return (
<article className="challenges-item">
…
</article>
);
}
Estos no son los únicos principios de diseño ni patrones de render que nos ayudan a diseñar componentes más saludables.
Es cierto que algunos pueden llegar a ser bastante complejos de entender si no los hemos usado antes. Pero también es cierto que son bastante populares, así que tarde o temprano los encontrarás.
Mi recomendación es que los estudies a profundidad para familiarizarte con su funcionamiento, comprendas en qué casos pueden ser muy útiles y no entres en pánico cuando te los encuentres en el código de alguien más.
Te invito a tomar la trilogía de cursos de React.js. Allí aprenderás sobre los principios de diseño, patrones de render y muchas más funcionalidades que puedes implementar en React.
#NuncaParesDeAprender 🤓💚
Excelente post, voy por el tercero y último curso. Así van mi TodoMachine de mis dos ramas en GitHub. Magnífico todo lo que se aprende ahí.
!
Juan cada día te quiero más! Estoy con el curso de Patrones de Render y Composición y este artículo me viene perfecto!
Consulta. Como se haría composición en una app que maneje rutas?
Una opción es usar composición dentro de cada ruta, aunque por el solo hecho de usar rutas estemos consumiendo un contexto. 😄