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 propiedades user y theme 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.

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