Intermedio
Composición de componentes
Es un patrón de diseño de componentes que se basa en crear un componente padre con un solo objetivo, proporcionarle a sus hijos las propiedades necesarias para que se rendericen sin problemas.
Permite una estructura declarativa a la hora de construir nuevos componentes, ademÔs ayuda a la lectura del código por su simplicidad y limpieza.
Un ejemplo de este diseƱo serĆa una lista que renderiza los elementos hijos:
<List>
<ListItem>Cat</ListItem>
<ListItem>Dog</ListItem>
</List>
const List = ({ children, ...props }) => (
<ul {...props} >
{children}
</ul>
);
const ListItem = ({ children, ...props }) => {
return (
<li {...props}>
{children}
</li>
);
};
export { List, ListItem };
Esto seria una alternativa a usar React Context.
Colocación del estado
MĆ”xima cercanĆa a la relevancia: El estado debe estar tan cerca como sea posible de donde lo estemos usando y actualizando.
Stateful vs stateless: Separar lógica y estado de componentes que manejan UI.
¿Dónde los guardamos? Este problema también se conoce como state colocation. Ejemplo:
Cuando varios componentes necesitan compartir los mismos datos de un estado, entonces se recomienda elevar ese estado compartido hasta su ancestro común mÔs cercano.
Dicho de otra forma. Si dos componentes hijos comparten los mismos datos de su padre, entonces mueve el estado al padre en lugar de mantener un estado local en sus hijos.
Para entenderlo, lo mejor es que lo veamos con un ejemplo.
import { useState } from 'react'
export default function App () {
return (
<>
<h1>Lista de regalos</h1>
<GiftList />
<TotalGifts />
</>
)
}
function GiftList () {
const [gifts, setGifts] = useState([])
const addGift = () => {
const newGift = prompt('¿Qué regalo quieres añadir?')
setGifts([...gifts, newGift])
}
return (
<>
<h2>Regalos</h2>
<ul>
{gifts.map(gift => (
<li key={gift}>{gift}</li>
))}
</ul>
<button onClick={addGift}>AƱadir regalo</button>
</>
)
}
function TotalGifts () {
const [totalGifts, setTotalGifts] = useState(0)
return (
<>
<h2>Total de regalos</h2>
<p>{totalGifts}</p>
</>
)
}
¿Qué pasa si queremos que el total de regalos se actualice cada vez que añadimos un regalo? Como podemos ver, no es posible porque el estado de totalGifts
estĆ” en el componente TotalGifts
y no en el componente GiftList
. Y como no podemos acceder al estado de GiftList
desde TotalGifts
, no podemos actualizar el estado de totalGifts
cuando aƱadimos un regalo.
Tenemos que subir el estado de gifts
al componente padre App
y le pasaremos el nĆŗmero de regalos como prop al componente TotalGifts
.
import { useState } from 'react'
export default function App () {
const [gifts, setGifts] = useState([])
const addGift = () => {
const newGift = prompt('¿Qué regalo quieres añadir?')
setGifts([...gifts, newGift])
}
return (
<>
<h1>Lista de regalos</h1>
<GiftList gifts={gifts} addGift={addGift} />
<TotalGifts totalGifts={gifts.length} />
</>
)
}
function GiftList ({ gifts, addGift }) {
return (
<>
<h2>Regalos</h2>
<ul>
{gifts.map(gift => (
<li key={gift}>{gift}</li>
))}
</ul>
<button onClick={addGift}>AƱadir regalo</button>
</>
)
}
function TotalGifts ({ totalGifts }) {
return (
<>
<h2>Total de regalos</h2>
<p>{totalGifts}</p>
</>
)
}
Con esto, lo que hemos hecho es elevar el estado. Lo hemos movido desde el componente GiftList
al componente App
. Ahora pasamos como prop los regalos al componente GiftList
y una forma de actualizar el estado, y tambiƩn hemos pasado como prop al componente TotalGifts
el nĆŗmero de regalos.
Render Props
Es un patrón que nos permite compartir código teniendo un componente el cual recibe una función render(u otro nombre) vĆa props la cual serĆ” el render que ejecutarĆ” el componente. Ejemplo, tenemos esto:
function App() {
...
return (
<TodoList>
{error && <TodosError />}
{loading && <TodosLoading />}
{(!loading && !searchedTodos.length) && <EmptyTodos />}
{searchedTodos.map(todo => (
<TodoItem
key={todo.text}
text={todo.text}
completed={todo.completed}
onComplete={() => completeTodo(todo.text)}
onDelete={() => deleteTodo(todo.text)}
/>
))}
</TodoList>
</React.Fragment>
);
}
// TodoList.jsx
function TodoList(props) {
return (
<section>
<ul>
{props.children}
</ul>
</section>
);
}
La ventaja de el patrón Render props nos permite hacer codigo mas declarativo siendo mas especificos para saber:
Que vamos a renderizar
Cuando lo vamos a renderizar
Donde lo vamos a renderizar
La implementación quedaria asĆ:
Usando Render Props
function App() {
...
return (
<TodoList
error={error}
loading={loading}
searchedTodos={searchedTodos}
onError={() => <TodosError />}
onLoading={() => <TodosLoading />}
onEmptyTodos={() => <EmptyTodos />}
render={todo => (
<TodoItem
key={todo.text}
text={todo.text}
completed={todo.completed}
onComplete={() => completeTodo(todo.text)}
onDelete={() => deleteTodo(todo.text)}
/>
)}
/>
);
}
Usando Render Function
<TodoList
error={error}
loading={loading}
totalTodos={totalTodos}
searchedTodos={searchedTodos}
searchText={searchValue}
onError={() => <TodosError />}
onLoading={() => <TodosLoading />}
onEmptyTodos={() => <EmptyTodos />}
onEmptySearchResults={
(searchText) => <p>No hay resultados para {searchText}</p>
}
>
{todo => (
<TodoItem
key={todo.text}
text={todo.text}
completed={todo.completed}
onComplete={() => completeTodo(todo.text)}
onDelete={() => deleteTodo(todo.text)}
/>
)}
</TodoList>
// TodoList.jsx
function TodoList(props) {
// Para render props o render function
// const renderFunc = props.render || props.children
return (
<section className="TodoList-container">
{props.error && props.onError()}
{props.loading && props.onLoading()}
{(!props.loading && !props.searchedTodos.length) && props.onEmptyTodos()}
{props.searchedTodos.map(props.render)}
// Para render function
// {props.searchedTodos.map(props.children)}
<ul>
{props.children}
</ul>
</section>
);
}
Tambien se pude substituir un render prop por un render function, la diferencia es que con function prop es que funtion envia en el children toda la lógica.
En React, los High Order Components (HOCs) y el patrón Render Props son dos patrones comunes para reutilizar la lógica entre componentes. Sin embargo, los HOCs pueden sufrir del "problema de colisión de nombres" cuando múltiples HOCs intentan pasar propiedades con el mismo nombre al componente envuelto. El patrón Render Props puede evitar este problema al delegar el control de la renderización y permitir al desarrollador elegir los nombres de las propiedades.
Ejemplo del Problema de Colisión con HOCs
Imaginemos que tenemos dos HOCs: uno para manejar el estado de autenticación y otro para manejar el estado de tema (tema claro u oscuro). Ambos HOCs podrĆan querer pasar una propiedad user
al componente envuelto.
HOC de Autenticación:
function withAuth(WrappedComponent) {
return function AuthComponent(props) {
const user = { name: "John Doe", authenticated: true };
return <WrappedComponent {...props} user={user} />;
};
}
HOC de Tema:
function withTheme(WrappedComponent) {
return function ThemeComponent(props) {
const user = { theme: "dark" };
return <WrappedComponent {...props} user={user} />;
};
}
Componente Envuelto:
const UserProfile = (props) => {
return (
<div>
<h1>{props.user.name}</h1>
<p>Theme: {props.user.theme}</p>
</div>
);
};
const EnhancedUserProfile = withAuth(withTheme(UserProfile));
// Renderizando <EnhancedUserProfile /> causarĆ” un problema
Problema:
Cuando se renderiza EnhancedUserProfile
, el segundo HOC (withTheme
) sobrescribirĆ” la propiedad user
pasada por el primer HOC (withAuth
). Esto resulta en un comportamiento inesperado, ya que el componente UserProfile
solo recibirÔ la última versión de user
, perdiendo asà los datos de autenticación.
Solución con Render Props
El patrón Render Props evita este problema al permitir que el componente hijo controle cómo se reciben y usan las propiedades. En lugar de pasar propiedades directamente al componente envuelto, se pasa una función que recibe los datos necesarios.
Implementación con Render Props:
Render Props para Autenticación:
const AuthProvider = ({ children }) => {
const user = { name: "John Doe", authenticated: true };
return children({ user });
};
Render Props para Tema:
const ThemeProvider = ({ children }) => {
const theme = { theme: "dark" };
return children({ theme });
};
Componente que Consume Ambas Propiedades:
const UserProfile = () => (
<AuthProvider>
{({ user }) => (
<ThemeProvider>
{({ theme }) => (
<div>
<h1>{user.name}</h1>
<p>Theme: {theme.theme}</p>
</div>
)}
</ThemeProvider>
)}
</AuthProvider>
);
// Renderizando <UserProfile /> funciona correctamente
Explicación:
AuthProvider pasa
user
a su función hija.ThemeProvider pasa
theme
a su función hija.En el componente
UserProfile
, ambas propiedadesuser
ytheme
se reciben sin colisiones, porque el desarrollador decide cómo nombrarlas y usarlas dentro de la función de render.
Conclusión:
El patrón Render Props evita el problema de colisión de nombres en HOCs al proporcionar una mayor flexibilidad en cómo se pasan y se nombran las propiedades. Cada parte de la lógica puede exponer sus datos sin interferir con otras, lo que lleva a un código mÔs claro y fÔcil de mantener.
React.Children y React.cloneElement
Se utiliza para poder pasar propiedades aparteque no estƔn dentro de children a los componentes hijos de nuestros componentes contenedores por alguna circunstancia.
function TodoHeader({ children, loading }) {
return (
<header>
{React.cloneElement(child, { loading: loading })}
</header>
);
}
El problema es que cuando enviamos mƔs de un componente o elemento hijo recibidas en las props del componente padre. CloneElement necesita recibir un elemento de react, cuando children es mƔs de un componente entonces tenemos un array, para esto existe React.Children que nos ayuda a que CloneElement entienda sin importar cuantos elementos vienen en el props.children.
// App.js
...
<TodoHeader loading={loading}>
<TodoCounter
totalTodos={totalTodos}
completedTodos={completedTodos}
/>
<TodoSearch
searchValue={searchValue}
setSearchValue={setSearchValue}
/>
</TodoHeader>
...
function TodoHeader({ children, loading }) {
return (
<header>
{React.Children.toArray(children).map((child) =>
React.cloneElement(child, { loading: loading })
)}
</header>
);
}
High Order Components (HOC)
Las funciones como las conocemos pueden devolvernos un valor en sus returns, pero estas funciones de "orden superior", son funciones que devuelven otras funciones (HOF).
Si llamamos a la High Order Function y le enviamos un parĆ”metro no tendremos todavĆa un resultado, como estĆ” devolviendo otra función tenemos que llamar a esa función que obtenemos luego de llamar a la de orden superior, enviarle los nuevos parĆ”metros que necesita la función de retorno y entonces si, obtendremos nuestro resultado.
Son funciones que retornan otras funciones aplicando el concepto funcional currying.
function highOrderFunction(var1) {
return function returnFunction(var2) {
return var1 + var2;
}
}
const withSum1 = highOrderFunction(1);
const sumTotal = withSum1(2);
Debido a que los componentes son funciones podemos tambiƩn aplicar este concepto.
// Caso base
function Componente(props){
return <p>...</p>
}
function highOrderComponent() {
return function Componente(props) {
return <p>...</p>
}
}
function highOrderComponent(WrappedComponent) {
return function Componente(props) {
return (
<WrappedComponent
{...algoEspecial}
{...props}
/>
);
}
}
De esta manera estamos personalizando varios aspectos del componente deseado, como:
Los parƔmetros de las funciones nos permiten configurar el componente que envuelve, las props
Podemos reutilizar los HOC
Ejemplos
function withApi(WrappedComponent) {
const apiData = fetchApi('https://api.com');
return function WrappedComponentWithApi(props) {
if (apidData.loading) return <p>Loading</p>;
return(
<WrapperdComponent data={apiData.json} />
);
}
}
Antes de retornar el componente en sĆ, hace una petición y entrega al componente esa información
AdemƔs que podemos personalizar el estado de carga
function TodoBox(props) {
return (
<p>
Tu nombre es {props.data.name}
</p>
);
}
const TodoBoxWithApi = withApi(TodoBox);
TambiƩn podemos agregar mƔs "capas" para tener mƔs personalizaciones como por ejemplo
function withApi(apiUrl){
return function withApiUrl(WrappedComponent) {
const apiData = fetchApi(apiUrl);
return function WrappedComponentWithApi(props) {
if (apidData.loading) return <p>Loading</p>;
return(
<WrapperdComponent data={apiData.json} />
);
}
}
}
function TodoBox(props) {
return (
<p>
Tu nombre es {props.data.name}
</p>
);
}
const TodoBoxWithApi = withApi('https://api.com')(TodoBox);
Esto nos permite poder extender funcionalidades.
HOCs
En React, un Higher-Order Component (HOC) es una función que toma un componente como argumento y devuelve un nuevo componente. Los HOCs son una forma avanzada de reutilizar la lógica de componentes entre múltiples componentes sin duplicar código.
¿Qué son y cómo funcionan los HOCs?
Un HOC es esencialmente una función que toma un componente WrappedComponent
y retorna un nuevo componente EnhancedComponent
. El HOC puede agregarle lógica, manejar props o estado, o ejecutar cualquier otro comportamiento adicional necesario, y luego pasarle los resultados al componente original.
function withLogging(WrappedComponent) {
return function EnhancedComponent(props) {
console.log('Rendering component with props:', props);
return <WrappedComponent {...props} />;
};
}
En este ejemplo, withLogging
es un HOC que envuelve WrappedComponent
y agrega un mensaje de log en la consola cada vez que el componente se renderiza.
¿Para qué sirven los HOCs?
Los HOCs son Ćŗtiles en varios escenarios, tales como:
Reutilización de lógica: Permiten compartir lógica entre componentes sin duplicar código.
Manipulación de props: Pueden modificar las props antes de que lleguen al componente envuelto.
Acceso a contextos: En aplicaciones anteriores a React Context, los HOCs facilitaban el acceso a datos compartidos.
Control de renderizado: Puedes condicionar la visualización de un componente, agregar un
loading
spinner o cualquier lógica relacionada con el renderizado.
Ejemplo de uso: Autorización
Supongamos que quieres restringir el acceso a una pÔgina en función del rol del usuario:
function withAuthorization(WrappedComponent) {
return function EnhancedComponent(props) {
const userRole = props.userRole;
if (userRole !== 'admin') {
return <p>Access Denied</p>;
}
return <WrappedComponent {...props} />;
};
}
const AdminPage = withAuthorization(SomeComponent);
AquĆ, el HOC withAuthorization
envuelve SomeComponent
y restringe su acceso si el usuario no tiene el rol de administrador.
ĀæSiguen siendo Ćŗtiles los HOCs?
Hoy en dĆa, los HOCs no se utilizan tan frecuentemente como antes, especialmente desde la introducción de React Hooks y Context API en versiones recientes de React. Las Hooks permiten manejar lógica compartida sin tener que recurrir a un HOC. Esto ha llevado a una reducción significativa en el uso de HOCs para tareas como la gestión de estado, el acceso a contextos y otras funciones compartidas.
¿CuÔndo son estrictamente necesarios los HOCs?
Integración con librerĆas antiguas: Algunos paquetes y librerĆas (como React-Redux antes de
useSelector
yuseDispatch
) aún ofrecen HOCs para inyectar funcionalidades en los componentes.Legacy code: En aplicaciones mÔs antiguas, los HOCs pueden seguir siendo la mejor manera de implementar ciertas funcionalidades sin una gran refactorización.
Render Props: Aunque también en desuso frente a Hooks, los HOCs pueden complementar o reemplazar la lógica de componentes de Render Props.
Resumen
Los HOCs fueron una herramienta esencial para la reutilización de lógica en React antes de la introducción de los Hooks, pero hoy en dĆa, la mayorĆa de las funcionalidades que implementan pueden ser logradas con Hooks y Context API, que son mĆ”s fĆ”ciles de entender y mĆ”s eficientes. Sin embargo, siguen siendo Ćŗtiles para integrar componentes de librerĆas antiguas y en aplicaciones legacy donde los Hooks podrĆan requerir una refactorización extensa.
Render props vs. High Order Components vs. React Hooks
Maquetación
Al utilizar React hooks
, estos nos proveen propiedades, información y actualizadores para que luego en el return de nuestros componentes podamos validaciones y renderizar o no ciertos componentes.
const { error, loading, todos } = customHook();
return {
<TodoList>
{ error && <TodosError /> }
...
</TodoList>
};
Pero cuando aplicamos Render props
dejamos de tener las validaciones y los componentes en el mismo lugar. Es decir, pasamos las validaciones al componente en cuestión, y simplemente debemos de enviar nuestros componentes a sus render props correspondientes. .
<TodoList
error={error}
...
onError={() => <TodosError />}
...
/>
Ambas son formas muy correctas y comunes de trabajar, pero las render props son mucho mÔs cómodas y ordenadas.
Share data
Empezando por las Render functions
, estas nos permiten compartir información con funciones que en sus parÔmetros nos dejan esa información que necesitÔbamos que nos compartieran.
<Consumer1>
{props1 => (
<Componente1 {...props1} />
)}
</Consumer1>
Pero si necesitamos demasiada información de distintas render functions para un mismo componente ya no es adecuado y tendrĆamos código espagueti. AsĆ que tiene poca escalabilidad.
<Componente1>
{props1 => (
<Componente2>
{props2 => (
<Componente3>
{props3 => (
{/* ... */}
)}
</Componente3>
)}
</Componente2>
)}
</Componente1>
Los High Order Components
, son funciones que devuelven otras funciones de esa manera sucesivamente hasta retornar un componente de React al que podamos pasarle toda nuestra información.
Sin embargo, si necesitamos la información de muchos hooks en un mismo componente entonces se tiene el mismo problema de antes, código muy horizontal lo que significa que no es escalable.
const TodoBoxWithEverything = withApi(
withSomething1(
withSomething2(
withSomething3(
withSomething4(
withSomething5(
TodoBox // ”Por fin!
)
)
)
)
)
);
Los React hooks
, solo lo llamamos y luego consumimos la información en el return de nuestro componente.
Si tenemos muchos llamados a distintos react hooks nuestro código sigue siendo extremadamente vertical y fÔcil de leer sin importar de cuantos react hooks estemos llamando. .
const [whatever1, setWathever1] = React.useState();
const [whatever2, setWathever2] = React.useState();
const [whatever3, setWathever3] = React.useState();
const [whatever4, setWathever4] = React.useState();
const [whatever5, setWathever5] = React.useState();
return (
<Componente {...todosLosWhatevers} />
);
Lo ideal para estos casos son los React Hooks o tambien los custom hooks, aunque hay manera de hacerlo tambien con HOC tambien debido a que se puede mejorar la legibilidad con la función compose.
const TodoBoxWithEverything = compose(
withApi,
withSomething1,
withSomething2,
withSomething3,
withSomething4,
withSomething5,
)(TodoBox);
Last updated