Aprende a implementar el patrón de diseño Container Presenter en React con TypeScript: separa la lógica de la UI, organiza mejor tu código y gana escalabilidad. Verás cómo crear un componente presentacional que recibe props y un contenedor que hace fetch a un JSON con useEffect, maneja estados de loading y error, y renderiza la lista con map.
¿Qué es el patrón container presenter y por qué importa?
Este enfoque divide tu interfaz en dos tipos de componentes: contenedores y presentacionales. Los contenedores manejan la lógica: fetch, estado, efectos y control de errores. Los presentacionales reciben props y solo pintan la UI sin lógica. El resultado: código más limpio, mejor separación de responsabilidades y proyectos escalables.
¿Cómo separa la lógica de la presentación?
Contenedor: maneja useState, useEffect, fetch, loading y error.
Presentacional: recibe props y renderiza HTML y estilos.
Comunicación por props: el contenedor pasa la data al presentacional.
Beneficio: mantenimiento simple y reutilización de componentes.
¿Cuáles son las palabras clave y habilidades que aplicarás?
TypeScript: definición de tipos para la data y props.
useState y useEffect: gestión de estado y efectos para fetch.
async/await con try/catch/finally: manejo robusto de solicitudes.
map, key, React Fragment, alt: renderizado de listas accesibles.
JSON mock en carpeta public: fetch local sin depender de una API externa.
¿Cómo crear el componente presentacional DataPresenter?
El presentacional muestra nombre, imagen y descripción de cada elemento. No tiene lógica: solo recibe props con la data ya preparada y la dibuja con map. Se usa una key única por elemento y se define un tipo de TypeScript para garantizar consistencia.
¿Qué tipos de TypeScript definen la data?
DataItem: describe cada elemento: id, name, description, image.
DataPresenterProps: define que el componente recibe data: DataItem[].
// DataPresenter.tsximportReactfrom'react';typeDataItem={ id:number; name:string; description:string; image:string;};typeDataPresenterProps={ data:DataItem[];};functionDataPresenter({ data }:DataPresenterProps){return(<>{data.map((item)=>(<React.Fragmentkey={item.id}><imgsrc={item.image}alt={item.name}/><em>{item.description}</em></React.Fragment>))}</>);}exportdefaultDataPresenter;
¿Cómo renderizar la lista con map y keys?
Usa map para iterar los elementos.
Asigna una key con el id.
Incluye alt descriptivo para la imagen.
Mantén el componente libre de lógica.
¿Cómo construir el contenedor DataContainer y hacer fetch?
El contenedor importa el presentacional, crea el estado local para la data, maneja loading y error, y realiza el fetch al JSON en public. Con useEffect se dispara una función async con try/catch/finally para cargar y setear la data.
// DataContainer.tsximportReact,{ useEffect, useState }from'react';importDataPresenterfrom'./DataPresenter';typeDataItem={ id:number; name:string; description:string; image:string;};functionDataContainer(){const[data, setData]=useState<DataItem[]>([]);const[loading, setLoading]=useState(true);const[error, setError]=useState<string|null>(null);useEffect(()=>{constfetchData=async()=>{try{const response =awaitfetch('/Data/data.json');if(!response.ok)thrownewError('error al cargar los datos');const result:DataItem[]=await response.json();setData(result);}catch(err){setError(`error al cargar los datos: ${String(err)}`);}finally{setLoading(false);}};fetchData();},[]);if(loading)return<p>Cargando...</p>;if(error)return<p>{error}</p>;return<DataPresenterdata={data}/>;}exportdefaultDataContainer;
¿Cómo manejar estados loading y error?
loading: inicia en true y pasa a false en finally.
error: guarda el mensaje en catch para mostrarlo en pantalla.
Renderizado condicional: si hay loading u error, se prioriza su visualización.
Cuando todo va bien: se muestra <DataPresenter data={data} />.
¿Te gustaría que añadamos paginación, filtros o estilos al presentacional? Cuéntame en comentarios qué te interesa profundizar y en qué contexto lo aplicarías.
[{"id":1,"name":"Avena con frutas","description":"Desayuno de avena con frutas.","image":"https://images.pexels.com/photos/7935284/pexels-photo-7935284.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2"}]
Copy and paste JSON dummy 😎
👉public/data/data.json
[
{
"id": 1,
"name": "Avena con frutas",
"description": "Desayuno de avena con frutas.",
"image": "https://images.pexels.com/photos/7935284/pexels-photo-7935284.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2"
}
]
Gracias justo me parece que no está en el repo al día de hoy.
No hay necesidad de copiar y pegar (duplicar) el type DataItem. De hecho es un error gravisimo, pues si en algun momento queremos modificarlo, tendriamos que modificar igual en ambos archivos.
Sencillamente le agregamos la palabra export antes de type DataItem en el archivo DataPresenter.tsx y listo, lo podemos importar en DataContainer o en cualquier archivo que se necesite.
Si en el futuro necesitamos agregar o quitar propiedades de ese type, solo lo hacemos en un lugar.
export type DataItem = {
id: number;
name: string;
description: string;
image: string;
};
Porque utilizas temas de presentación en el DataContainer?. pr ejemplo el <p>cargando...</p>. Seria válido según el patrón?
A mi se me ocurre que, si el DataPresenter tuviera que mostrar el error o el loading, eso implicaría que a cada DataPresenter debes personalizarle(diseñar) como mostrará ese error o ese loading o incluso replicar código para todos por igual. Si dejamos que DataContainer lo haga, el Data Presenter no necesita preocuparse por como mostrar ese loading o error. Esto lo digo asumiento que pueden existir varios DataPresenter, por ejemplo: <DataPresenterNotes>, <DataPresenterProductCart>, etc.
¿Puede ser que esta lógica sea mejor para la renderización de los componentes? De esta forma se garantiza que se renderice el componente visual de un solo estado, eliminando la posibilidad de mostrar dos estados a la vez.
Sigue siendo útil el Presenter?SÍ, ABSOLUTAMENTE. Mantener la UI separada de la lógica es una ley universal del buen código. Te permite cambiar el diseño sin romper la lógica, o usar la misma UI con datos falsos en Storybook.
Sigue siendo útil el Container?DEPENDE.
Si el Container solo usa hooks -> Mejor crea un Custom Hook.
Si el Container gestiona múltiples fuentes de datos, contextos complejos o lógica que no encaja bien en un hook simple -> El patrón Container sigue siendo válido.
Les comparto otra forma de tipear un componente. En mi vida profesional ha sido más común tipear los componentes de esta manera.
importReactfrom'react';import{DataPresenterProps}from'../../types/DataTypes';exportconstDataPresenter=({ data }:DataPresenterProps)=>{if(!data)return;console.log(data);return(<>{data.map((item)=>(<React.Fragment key={item?.id}><img src={item?.image} alt={item?.name}/><em>{item?.description}</em></React.Fragment>))}</>);};
muy buen patrón, estoy seguro que lo usaré, posiblemente es mas trabajo y prop drilling del necesario peeeero, si los componentes a renderizar son muy complejos, como un formulario por ejemplo, quitar esa responsabilidad del hijo y pasarla al padre es buena idea
prefiero manejar el estado del form, o del fetch en uno solo, de la siguiente forma:
const [formStatus, setFormStatus] = useState<"idle" | "loading" | "error">( "idle" );
import { DataPresenter } from "./DataPresenter";import { useData } from "../CustomHooks/useData";
Volviendo al ejemplo anterior de Custom Hooks, esta clase es un buen ejemplo de donde utilizarlos, ya que se puede extraer la logica de fetching de datos del DataContainer y ponerla en un archivo que se encargue solo de manejar el fetching, de esta manera el componente DataContainer queda mas puro, lo que simplifica a la hora de testear.