Principios SOLID en React
Los principios SOLID nacieron en el contexto del diseño orientado a objetos y, en su
formulación clásica, se refieren sobre todo al diseño de clases. React, en cambio,
recomienda hoy componentes funcionales por encima de clases y favorece la
composición por encima de la herencia. Por eso, aplicar SOLID en React no consiste
en “traducir clases Java/C# a componentes”, sino en reinterpretar cada principio
sobre otras unidades de diseño: componentes, hooks, módulos, providers,
adaptadores y contratos de props/context. Esa adaptación no es una licencia
arbitraria, sino una consecuencia del propio modelo de React y del origen histórico
de SOLID. [1]
En React, los principios que suelen dar mejor retorno práctico son SRP, ISP y DIP,
porque atacan problemas muy frecuentes en frontend: componentes que mezclan
renderizado con sincronización externa, props o contextos demasiado “anchos”, y
dependencias directas de la UI respecto de SDKs, APIs o detalles de infraestructura.
OCP también es muy potente cuando diseñas componentes reutilizables o sistemas
de diseño, pero hoy suele expresarse mediante composición, children, slots, render
props o hooks, no mediante jerarquías de herencia. LSP sigue siendo importante,
pero en React se desplaza del terreno “subclase/superclase” al terreno “contrato de
comportamiento”: una variante, wrapper o hook alternativo debe poder sustituir al
original sin romper las expectativas del consumidor. [2]
La conclusión operativa es esta: SOLID sí aporta valor en React, pero como heurística
arquitectónica, no como receta literal. Aplicarlo bien significa diseñar componentes
puros y fáciles de razonar, extraer efectos innecesarios, usar contexto con prudencia,
preferir interfaces pequeñas y desacoplar la UI de los detalles concretos. Aplicarlo
mal significa sobrefragmentar el código, introducir “wrapper hell”, abusar de
contextos y crear abstracciones prematuras que hacen más difícil mantener la
aplicación. React advierte explícitamente contra los Efectos innecesarios y contra el
uso indiscriminado de Context, lo que encaja muy bien con una lectura pragmática
de SOLID. [3]
Cómo traducir SOLID al modelo mental de React
Robert C. Martin reunió los cinco principios SOLID como principios de diseño de
clases. React, por su parte, propone pensar la UI separándola en piezas pequeñas,
describir estados visuales y conectar esas piezas mediante un flujo de datos claro.
Esa diferencia de paradigma obliga a desplazar el foco: en React, la “unidad de
diseño” no es solo la clase, sino también el componente funcional, el custom hook,
el archivo de estado/contexto y el adaptador que conecta con un sistema externo.
Esta es una inferencia de diseño coherente con las fuentes: SOLID parte del diseño
de clases; React actual parte de funciones, composición y flujo de datos. [4]
Una adaptación útil y conservadora de cada principio a React puede resumirse así.
SRP: un componente, hook o módulo debería tener una sola razón principal para
cambiar; en React eso suele traducirse en separar presentación, sincronización
externa y reglas de negocio. OCP: conviene extender el comportamiento sin
reescribir el “núcleo” estable; en React esto suele hacerse con composición,
children, props de renderizado o hooks reutilizables. LSP: una variante debe
mantener el contrato observable del original; en React, el contrato es el
comportamiento de props, eventos, accesibilidad, renderizado y valores de contexto,
no solo la forma del tipo. ISP: los consumidores no deberían depender de props,
contextos o hooks “gordos” que exponen capacidades que no usan. DIP: la pantalla
o la lógica de alto nivel debería depender de contratos estables, no de SDKs o
clientes HTTP concretos; en React eso se consigue con providers, custom hooks y
adaptadores inyectables. [5]
React recomienda explícitamente componentes funcionales en código nuevo,
considera a HOCs y render props como patrones válidos pero menos comunes en el
React moderno, y ubica gran parte de su reutilización contemporánea en la
composición y los custom hooks. Además, insiste en que componentes y hooks sean
puros, en que los Efectos son una “vía de escape” para sincronizar con sistemas
externos, y en que Context es útil pero fácil de sobreusar. Ese conjunto de reglas
prácticas es, de hecho, el terreno natural en el que SOLID se vuelve más útil en
aplicaciones React reales. [6]
Una observación importante, y a menudo olvidada: el frontend de UI tiene límites
prácticos que no aparecen igual en backend. Un mismo componente puede
necesitar mezclar marcado, accesibilidad, control de eventos y un poco de estado
local sin que eso sea necesariamente un mal diseño. Por eso, en React el criterio
correcto no es “hacer piezas microscópicas”, sino “agrupar lo que cambia por las
mismas razones y separar lo que cambia por razones distintas”, que es precisamente
el espíritu original de SRP. [7]
SRP en React
Qué significa en React
La formulación clásica de SRP define una responsabilidad como una razón para
cambiar. En React, eso suele traducirse en algo muy concreto: un componente de UI
no debería cambiar a la vez porque cambió la maqueta, porque cambió el endpoint,
porque cambió la instrumentación de analytics y porque cambió la normalización de
datos. Si todo eso vive junto, el componente tiene varias razones de cambio. [8]
React refuerza esta lectura porque pide pureza en componentes y hooks, y porque
distingue entre render, eventos y Efectos. La documentación oficial insiste en que no
deberías usar Efectos para transformar datos derivados ni para manejar eventos de
usuario; esos usos mezclan responsabilidades y complican el flujo de datos. En otras
palabras, SRP en React no es una regla estética: es una herramienta para mantener
clara la frontera entre cálculo de UI, eventos y sincronización con sistemas externos.
[9]
Motivación y cuándo aplicarlo
Aplica SRP cuando un componente empieza a parecerse a una “mini aplicación”:
obtiene datos, hace caché local, transforma resultados, emite métricas, renderiza
varias ramas condicionales y además manipula el DOM o sincroniza un widget
externo. También conviene aplicarlo cuando varias historias de usuario no
relacionadas tocan el mismo archivo, o cuando el reasoning local sobre un hook deja
de ser evidente. React considera el “local reasoning” una propiedad importante:
deberías poder entender qué hace un componente o hook leyendo su código en
aislamiento. [10]
En la práctica, SRP en React suele aplicarse en tres cortes habituales: separar
componente contenedor de componente de vista; extraer lógica de sincronización o
fetching a un custom hook; y mover wiring de estado compartido a un provider o
módulo específico. Para código nuevo, React recomienda componentes funcionales
en vez de clases, así que este principio suele aterrizar mejor en funciones pequeñas y
hooks que en jerarquías de clases. [11]
Patrones y técnicas recomendadas
El patrón más fiable es vista + hook: el hook encapsula la sincronización o la
recuperación de datos, y el componente de vista se limita a describir la UI. Otro
patrón útil es separar el estado compartido en el ancestro más cercano cuando
varios hijos deben coordinarse; React lo describe como “lifting state up”. Si el estado
debe viajar muy profundo y forma parte de una preocupación transversal, puedes
usar Context, pero no como reflejo automático: React recomienda probar antes con
props explícitas o con children, porque dejan el flujo de datos más visible. [12]
También conviene evitar el “efecto comodín”. React dice que, si no hay un sistema
externo involucrado, probablemente no necesitas un Efecto; derivar estado con
useEffect o poner lógica de eventos dentro de efectos hace más difícil seguir el flujo
y añade renderizados innecesarios. Ese consejo es casi una formulación operativa de
SRP para UI. [13]
Ejemplos breves
TypeScript
import { useEffect, useState } from "react";
type User = { id: string; name: string; email: string };
function useUser(userId: string) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
let cancelled = false;
fetch(`/api/users/${userId}`)
.then((r) => r.json())
.then((data: User) => {
if (!cancelled) setUser(data);
});
return () => {
cancelled = true;
};
}, [userId]);
return user;
}
function UserCardView({ user }: { user: User }) {
return (
<article>
<h2>{user.name}</h2>
<p>{user.email}</p>
</article>
);
}
export function UserCard({ userId }: { userId: string }) {
const user = useUser(userId);
if (!user) return <p>Cargando…</p>;
return <UserCardView user={user} />;
}
JavaScript
import { useEffect, useState } from "react";
function useUser(userId) {
const [user, setUser] = useState(null);
useEffect(() => {
let cancelled = false;
fetch(`/api/users/${userId}`)
.then((r) => r.json())
.then((data) => {
if (!cancelled) setUser(data);
});
return () => {
cancelled = true;
};
}, [userId]);
return user;
}
function UserCard({ userId }) {
const user = useUser(userId);
if (!user) return <p>Cargando…</p>;
return <h2>{user.name}</h2>;
}
Anti‑patrón típico
// Evitar: estado derivado + efecto innecesario
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
Ese código viola SRP de forma sutil: la responsabilidad de derivar UI a partir de estado
entra en un Efecto, cuando React recomienda calcularla directamente durante el
render. [14]
Anti‑patrones y límites prácticos
Los anti‑patrones más comunes son el “componente dios”, el estado derivado
guardado en useState más un useEffect, la lógica específica de eventos desplazada
a Efectos y la mutación de valores no locales durante render. React documenta todos
ellos como fuentes de bugs o renderizados extra, y Strict Mode ayuda a hacerlos
visibles precisamente porque vuelve a renderizar y re‑ejecuta Efectos en desarrollo.
[15]
La limitación práctica es que una UI cohesionada necesita cierto volumen de código
en el mismo sitio. Un formulario con validación, accesibilidad, foco, estados visuales
y submit puede seguir respetando SRP si todas esas piezas cambian por el mismo
motivo funcional. Romperlo demasiado pronto produce archivos diminutos,
navegación incómoda y peor productividad. SRP en React debe perseguir cohesión,
no atomización ciega. Esa conclusión es una inferencia coherente entre la definición
original de “razón de cambio” y las reglas de pureza/flujo de React. [16]
Métricas y señales de violación
Las señales más útiles son combinadas. Si un componente supera con frecuencia
los límites configurados de complexity, max-lines-per-function o max-lines, hay
una sospecha razonable de que acumula varias responsabilidades; ESLint explica
que estas reglas existen precisamente para limitar complejidad y mejorar
mantenibilidad. A eso se suman síntomas React‑específicos: varios useEffect no
relacionados en el mismo componente; necesidad recurrente de silenciar reglas de
hooks; y errores que Strict Mode revela al re‑renderizar o re‑ejecutar Efectos. [17]
OCP en React
Qué significa en React
OCP dice que una entidad de software debería poder extenderse sin modificar su
núcleo estable. En la formulación de Martin, la idea es añadir nuevo código en vez de
cambiar código ya probado; y React añade una pista decisiva: cuando quieres
reutilizar comportamiento entre componentes, la recomendación general es
composición, no herencia. Por tanto, OCP en React se expresa mejor como “diseñar
componentes extensibles por composición, slots, children, render props, custom
hooks o providers”, no como “crear árboles de subclases”. [18]
La documentación legacy de React sigue describiendo HOCs y render props como
patrones útiles de reutilización para preocupaciones transversales, pero también
reconoce que hoy los HOCs no son comunes en React moderno y que muchos casos
de render props han sido reemplazados por custom hooks. Eso encaja muy bien con
OCP: el patrón concreto puede cambiar, pero la intención se mantiene, que es
extender sin reescribir el componente base. [19]
Motivación y cuándo aplicarlo
Aplica OCP cuando estás construyendo piezas reutilizables: tablas, listas, modales,
componentes “headless”, filtros, layouts, primitivas de diseño o componentes de
librería interna. Si cada nueva variante obliga a tocar un switch central, añadir flags
incompatibles o editar varias ramas del mismo componente, el diseño está pidiendo
una extensión por composición. Martin muestra justamente ese problema con
funciones que deben modificarse para cada nuevo tipo; en React el equivalente
típico es el componente que crece mediante if/else y variant === ... hasta
convertirse en una matriz de casos. [20]
Es especialmente útil en bibliotecas de UI o “design systems”. Ahí interesa estabilizar
el contrato del componente base y permitir especializaciones por children,
renderItem, as, providers, tema o hooks. Pensar así reduce el número de sitios que
cambian cuando añades una capacidad nueva. [21]
Patrones y técnicas recomendadas
El patrón más idiomático en React es la composición. React la recomienda
explícitamente frente a la herencia. Sobre esa base puedes construir APIs extensibles
con children, “slots” explícitos, componentes compuestos, prop‑getters, render
props o hooks “headless” que exponen estado y acciones mientras delegan al
consumidor el marcado final. Kent C. Dodds resume bien el trade‑off: los patrones
avanzados de componentes sirven para hacer componentes más flexibles, con
intercambios claros entre simplicidad inicial y extensibilidad. [21]
Los HOCs siguen siendo válidos para concerns transversales y son comunes en
ecosistemas legacy, pero React advierte varios caveats: un HOC no debe crearse
dentro de render, porque eso provoca desmontajes/remontajes del subárbol y
pérdida de estado; además, los HOCs mutantes son una abstracción con fugas y
funcionan mal con componentes funcionales. Los render props siguen siendo útiles
cuando quieres delegar la representación al consumidor, pero en componentes tipo
PureComponent pueden anular la optimización si la función se recrea en cada render.
En React actual, si la necesidad real es compartir lógica con estado, los custom
hooks suelen ser la salida más limpia. [22]
Ejemplos breves
TypeScript
type ListProps<T> = {
items: T[];
empty?: React.ReactNode;
renderItem: (item: T) => React.ReactNode;
};
export function List<T>({ items, empty = null, renderItem }:
ListProps<T>) {
if (items.length === 0) return <>{empty}</>;
return <ul>{items.map(renderItem)}</ul>;
}
// Extensión sin modificar List:
<List
items={users}
empty={<p>Sin usuarios</p>}
renderItem={(user) => <li key={user.id}>{user.name}</li>}
/>
JavaScript
function Fetcher({ url, children }) {
const [data, setData] = React.useState(null);
React.useEffect(() => {
fetch(url).then((r) => r.json()).then(setData);
}, [url]);
return children(data);
}
// Extensión por render prop:
<Fetcher url="/api/products">
{(products) =>
products ? <ProductGrid products={products} /> : <p>Cargando…</p>
}
</Fetcher>
Anti‑patrón típico
function List({ items, type }) {
if (type === "users") return items.map((u) => <UserRow key={u.id}
user={u} />);
if (type === "orders") return items.map((o) => <OrderRow key={o.id}
order={o} />);
if (type === "invoices") return items.map((i) => <InvoiceRow key={i.id}
invoice={i} />);
// cada nuevo tipo obliga a modificar el mismo componente
}
Anti‑patrones y límites prácticos
El anti‑patrón más obvio es la explosión de variantes: el componente base acumula
ramas condicionales, props mutuamente excluyentes y conocimiento creciente de
todos los casos posibles. Otro anti‑patrón es el HOC dentro de render, que React
documenta como causa de remount y pérdida de estado. También son peligrosas las
abstracciones mutantes en HOCs y los render props que fuerzan rerenders
innecesarios en ciertos componentes optimizados. [23]
La limitación práctica de OCP es el sobre‑diseño. Diseñar puntos de extensión “por si
acaso” hace que una API simple se convierta en una API genérica y difícil de usar. En
UI, muchas veces basta con una prop concreta; convertirla demasiado pronto en un
render prop, un sistema de slots y tres providers puede empeorar la ergonomía. OCP
funciona mejor cuando la variabilidad ya es real y recurrente, no cuando es
hipotética. Esto es una conclusión inferida de la intención del principio y de los
trade‑offs que describen React y Kent C. Dodds para patrones avanzados. [24]
Métricas y señales de violación
La señal principal es histórica y estructural: añadir un nuevo caso obliga a editar el
componente “núcleo” en vez de añadir una nueva implementación o composición.
Otras señales son un crecimiento sostenido de switch/if por variant, múltiples
props bandera que se pisan entre sí y la aparición de ramas específicas por cliente
dentro de componentes supuestamente genéricos. En runtime, el Profiler de React
DevTools ayuda a detectar capas de wrappers o rerenders que surgen de
HOCs/render props mal aplicados. [25]
LSP en React
Qué significa en React
La formulación clásica de LSP exige que una derivación pueda sustituir a su base sin
romper las propiedades deseables del programa. En React moderno, donde la
herencia es secundaria, el principio no desaparece: cambia de lugar. La pregunta
deja de ser “¿esta subclase sustituye a la clase base?” y pasa a ser “¿esta variante,
wrapper, implementación o hook alternativo preserva el contrato observable que
esperan los consumidores?”. [26]
Ese contrato observable puede incluir cosas como: qué props acepta, cuándo
dispara callbacks, si respeta disabled, cómo se comporta con children, si mantiene
accesibilidad semántica, si el valor del contexto conserva invariantes y si la
sustitución deja intacto el comportamiento esperado. LSP es, en esencia, un
principio conductual. Martin subraya que el problema de Square y Rectangle no era
nominal, sino de comportamiento; y Liskov y Wing formalizan esa idea diciendo que
las propiedades probables del supertipo deben seguir valiendo para el subtipo. [27]
Motivación y cuándo aplicarlo
LSP importa mucho cuando diseñas primitivas de sistema de diseño, componentes
intercambiables, adapters, proveedores reales y falsos para test, o custom hooks
con varias implementaciones. Si el consumidor necesita meter if (esX) antes de
usar la variante, la sustitución ya está fallando. En React, esto aparece a menudo no
en jerarquías de clases, sino en alias de componentes que “parecen” compatibles
pero cambian la semántica: un botón que deja de comportarse como botón, un input
controlado que pasa a no serlo, o un wrapper que altera el timing de eventos. [28]
TypeScript ayuda, pero no resuelve todo. Su compatibilidad es estructural y el propio
handbook reconoce que el sistema no es completamente sound. Eso quiere decir
que la asignación por tipo puede pasar aunque el comportamiento real no respete el
contrato. En React, por tanto, LSP se valida con tipos y con pruebas de
comportamiento, historias de Storybook y revisión semántica de
accesibilidad/eventos. [29]
Patrones y técnicas recomendadas
El patrón central es diseñar contratos pequeños y explícitos. Si dos
implementaciones quieren ser sustituibles, deben compartir un conjunto de props y
semánticas cuyo comportamiento sea estable. A eso ayuda mucho preferir
composición sobre herencia y pensar en “roles” claros: ButtonLike, SelectionApi,
Formatter, ProductGateway. Cuando el contrato es pequeño, es más fácil preservar
invariantes. [30]
También conviene acompañar los tipos con pruebas de sustitución. La parte formal
de LSP no se reduce a que el tipo compile; se trata de que lo que era verdad para el
consumidor siga siendo verdad tras sustituir una implementación. En React, esto
suele traducirse en tests donde varias implementaciones pasan por el mismo
conjunto de casos, y en snapshots o stories equivalentes para variantes visuales.
Esta recomendación es inferencial, apoyada en el carácter conductual de LSP y en
las limitaciones reconocidas del tipado estructural de TypeScript. [31]
Ejemplos breves
TypeScript
type ButtonLikeProps = {
children: React.ReactNode;
disabled?: boolean;
onPress?: () => void;
};
export function PrimaryButton({
children,
disabled = false,
onPress,
}: ButtonLikeProps) {
return (
<button disabled={disabled} onClick={disabled ? undefined : onPress}>
{children}
</button>
);
}
export function DangerButton({
children,
disabled = false,
onPress,
}: ButtonLikeProps) {
return (
<button
className="danger"
disabled={disabled}
onClick={disabled ? undefined : onPress}
>
{children}
</button>
);
}
Aquí ambas implementaciones son sustituibles porque preservan el mismo contrato
observable.
JavaScript
function SaveButton({ disabled = false, onPress, children }) {
return (
<button disabled={disabled} onClick={disabled ? undefined : onPress}>
{children}
</button>
);
}
function DeleteButton({ disabled = false, onPress, children }) {
return (
<button
className="danger"
disabled={disabled}
onClick={disabled ? undefined : onPress}
>
{children}
</button>
);
}
Anti‑patrón típico
function LinkButton({ disabled, onPress, children }) {
return (
<a href="/next" onClick={onPress}>
{children}
</a>
);
}
Ese wrapper parece “tipo botón”, pero ignora disabled y cambia la semántica del
elemento. El consumidor que asumía el contrato de un botón ya no puede sustituirlo
sin riesgo.
Anti‑patrones y límites prácticos
En React, LSP suele romperse cuando una variante comparte la forma del API pero
cambia su comportamiento público: callbacks con otra semántica, estado
controlado/no controlado distinto, props ignoradas, valores de contexto con
invariantes rotos o wrappers que requieren que el consumidor “sepa demasiado”
sobre cómo están implementados. Martin lo resume muy bien: si el cliente debe
conocer derivados concretos, el diseño rompe OCP y LSP a la vez. [32]
La limitación práctica es que el comportamiento de UI es mucho más semántico que
nominal. Un tipo puede ser compatible y, aun así, una variante romper accesibilidad,
foco, interacción por teclado o timing de eventos. Por eso, LSP en React no se
satisface con interfaces de TypeScript bien escritas: requiere también casos de
prueba que capturen el comportamiento esperado. [33]
Métricas y señales de violación
Las señales más claras son: los consumidores añaden condicionales según la
implementación concreta; el mismo conjunto de tests no pasa para todas las
variantes; el tipo es asignable pero las historias o pruebas de interacción divergen; y
la documentación del componente termina añadiendo excepciones del tipo “acepta
las mismas props, excepto…”. En TypeScript, otra señal útil es depender demasiado
de la compatibilidad estructural para dar por bueno un reemplazo, sabiendo que el
propio lenguaje admite zonas de falta de soundness. [33]
ISP en React
Qué significa en React
ISP dice que los clientes no deberían verse forzados a depender de interfaces que no
usan. En React, “interfaz” suele significar props, valor de contexto, retorno de un
hook o shape de un objeto de configuración. El olor equivalente del frontend es muy
reconocible: componentes con veinte props opcionales, hooks que devuelven
“bolsas” enormes de estado y acciones, o contextos gigantes que obligan a que
muchos consumidores dependan de más información de la que realmente
necesitan. [34]
React ofrece dos ideas importantes aquí. Primera: el flujo explícito por props, aunque
sea algo tedioso, deja claro qué componente usa qué datos. Segunda: Context es
tentador, pero fácil de sobreusar; no porque pases props a varios niveles ya debes
moverlo todo a contexto. Esta advertencia oficial encaja de forma casi exacta con
ISP: el problema no es solo el “prop drilling”, sino también el “context dumping”. [35]
Motivación y cuándo aplicarlo
Aplica ISP cuando mantienes componentes compartidos, librerías internas, providers
de aplicación o hooks reutilizables. Si cada consumidor usa un subconjunto distinto y
pequeño de la API, la interfaz probablemente está demasiado cargada. Si muchos
componentes solo leen una parte mínima de un contexto enorme, o reciben
callbacks que jamás invocan, estás pagando acoplamiento extra. [36]
En React, ISP también sirve para combatir el “mega hook”, una forma moderna de
interfaz gorda. Un hook cuyo retorno incluye datos, setters, estado derivado,
referencias, flags y side effects obliga a clientes distintos a depender del mismo
paquete. La alternativa suele ser separar hooks por rol o, cuando el estado es
compartido, separar contextos o custom hooks de lectura y escritura. React muestra
precisamente un patrón de dos contextos —estado y dispatch— para mantener
responsabilidades y consumo más enfocados. [37]
Patrones y técnicas recomendadas
El patrón más útil es dividir por rol del cliente. En props, significa definir superficies
pequeñas y orientadas al caso de uso. En contexto, significa evitar un objeto
monolítico cuando distintos consumidores leen o escriben partes diferentes; React
enseña a dividir en dos contextos el estado y el dispatch, y luego a exponer hooks
específicos como useTasks() y useTasksDispatch(). Ese ejemplo es muy valioso
porque reduce acoplamiento semántico y mantiene los componentes más limpios.
[37]
Otra técnica importante es resistir el reflejo de mover cualquier dato profundo a
Context. React recomienda probar antes con props o children, precisamente porque
eso mantiene el flujo de datos explícito. Context es apropiado para temas, rutas,
usuarios autenticados, estado transversal o pantallas complejas; no para cualquier
dato que viaje dos o tres niveles. [35]
Ejemplos breves
TypeScript
import { createContext, useContext } from "react";
type Toast = { id: string; message: string };
const ToastStateContext = createContext<Toast[] | null>(null);
const ToastActionsContext = createContext<{
add(message: string): void;
remove(id: string): void;
} | null>(null);
export function useToastState() {
return useContext(ToastStateContext);
}
export function useToastActions() {
return useContext(ToastActionsContext);
}
Un componente que solo necesita leer toasts no depende de add/remove, y uno que
solo dispara acciones no depende del array completo.
JavaScript
const ThemeContext = React.createContext("light");
function Page({ children }) {
return <main>{children}</main>;
}
// Mantén props explícitas cuando el alcance es pequeño:
function Toolbar({ theme, onSave }) {
return <button onClick={onSave}>{theme}</button>;
}
Aquí no usar Context también puede ser una decisión ISP correcta, porque el cliente
depende solo de lo que usa y el flujo es claro.
Anti‑patrón típico
type DashboardContextValue = {
user: User;
theme: string;
locale: string;
filters: Filters;
setFilters: (f: Filters) => void;
refresh: () => void;
exportCsv: () => void;
notifications: Notification[];
dismissNotification: (id: string) => void;
};
Ese shape “sirve para todo”, pero obliga a clientes pequeños a depender del mismo
contrato grande.
Anti‑patrones y límites prácticos
Los anti‑patrones más habituales son las fat props, los fat contexts y los hooks
todoterreno. Otro olor común es sustituir todo prop drilling por Context sin
preguntarse si la información es realmente transversal; React advierte exactamente
contra eso y recuerda que pasar props mantiene explícito qué datos usa cada pieza.
[38]
La limitación práctica es que llevar ISP demasiado lejos produce provider hell y APIs
excesivamente fragmentadas. Separar contexto por cada campo, o crear hooks
ínfimos para cosas que siempre viajan juntas, puede empeorar la ergonomía y la
trazabilidad. De nuevo, el criterio sano es el del consumidor: separar cuando los
clientes realmente usan subconjuntos distintos y su ciclo de cambio es diferente.
[39]
Métricas y señales de violación
Las señales más fiables son: interfaces de props con una mayoría de campos
opcionales; consumidores que solo usan una pequeña fracción del contexto o del
retorno del hook; documentación llena de props “solo aplican en tal variante”; y
contextos tan amplios que actualizaciones no relacionadas hacen re‑renderizar
partes lejanas del árbol. React recuerda que Context pasa “a través” de
componentes intermedios y, por tanto, conviene usarlo cuando el dato realmente
pertenece a una preocupación compartida. Si además necesitas props o children
para evitar contexto, esa suele ser la pista de que la interfaz estaba demasiado
grande o mal ubicada. [40]
DIP en React
Qué significa en React
DIP establece dos ideas: los módulos de alto nivel no deberían depender de módulos
de bajo nivel; ambos deberían depender de abstracciones, y las abstracciones no
deberían depender de detalles, sino al revés. En React, el módulo de alto nivel suele
ser la pantalla, el flujo de UI o la lógica de negocio visible; los de bajo nivel suelen ser
fetch, un SDK de analytics, un cliente HTTP, una librería de feature flags o un wrapper
de almacenamiento. La abstracción puede ser una interfaz TypeScript, un contrato
de callback, un valor de contexto o un custom hook. [41]
La forma más idiomática de expresar DIP en React es provider + hook + adaptador.
El provider inyecta la implementación concreta; el hook expone la abstracción
estable que consume la UI; y el componente de nivel superior depende del contrato,
no del detalle. React no lo presenta con el vocabulario clásico de DIP, pero su
documentación sobre Context, Reducer y custom hooks encaja exactamente con esa
inversión de dependencias. [42]
Motivación y cuándo aplicarlo
Aplica DIP cuando una pantalla necesita acceder a datos, autenticación, analítica,
permisos, feature flags o cualquier servicio externo. Si el componente importa
directamente un SDK o un cliente concreto, cada cambio de infraestructura puede
propagarse hacia arriba. Martin explica que cuando los módulos de alto nivel
dependen de los de bajo nivel, los cambios de estos últimos pueden forzar cambios
en los primeros y hacer difícil su reutilización. Esa observación es igual de válida en
frontend. [43]
También conviene aplicarlo en tests y en aplicaciones con varios entornos. Si puedes
inyectar un gateway falso, un provider de pruebas o un adaptador de memoria, la UI
se vuelve mucho más comprobable. React, además, recomienda extraer wiring de
estado y contexto a un archivo separado y exponer hooks propios; eso desacopla los
componentes de la mecánica concreta de provisión de dependencias. [37]
Patrones y técnicas recomendadas
El patrón más claro es inyectar detalles mediante provider y consumirlos con un
custom hook. createContext proporciona el canal; el provider suministra la
implementación; el hook la lee y ofrece una API declarativa a los componentes. React
también muestra que, cuando el estado crece, combinar reducer + context permite
centralizar la lógica de actualización y evitar pasar estado y handlers por props.
Además, separar los dos contextos —lectura y dispatch— mantiene mejor el contrato
de cada cliente. [44]
Los providers no son la única forma de aplicar DIP. En componentes de hoja o
componentes muy pequeños, pasar una función o un objeto adaptador por props
puede ser suficiente y hasta preferible. DIP no exige un contenedor de inyección de
dependencias; exige que el alto nivel dependa de un contrato estable. En React,
muchas veces ese contrato es simplemente una prop callback bien definida. Esta
conclusión es una inferencia consistente entre el principio clásico y el modelo de
composición/props de React. [45]
Ejemplos breves
TypeScript
import { createContext, useContext, useEffect, useState } from "react";
type Product = { id: string; name: string };
interface ProductGateway {
list(): Promise<Product[]>;
}
const ProductGatewayContext = createContext<ProductGateway | null>(null);
export function ProductsProvider({
gateway,
children,
}: {
gateway: ProductGateway;
children: React.ReactNode;
}) {
return (
<ProductGatewayContext value={gateway}>
{children}
</ProductGatewayContext>
);
}
export function useProducts() {
const gateway = useContext(ProductGatewayContext);
const [products, setProducts] = useState<Product[]>([]);
useEffect(() => {
if (!gateway) return;
gateway.list().then(setProducts);
}, [gateway]);
return products;
}
JavaScript
function SaveProfileButton({ saveProfile }) {
const [saving, setSaving] = React.useState(false);
async function handleClick() {
setSaving(true);
await saveProfile();
setSaving(false);
}
return (
<button onClick={handleClick} disabled={saving}>
{saving ? "Guardando..." : "Guardar"}
</button>
);
}
En el ejemplo JavaScript, la UI depende del contrato saveProfile, no de fetch, axios
o un SDK concreto.
Anti‑patrón típico
import axios from "axios";
function ProfilePage() {
React.useEffect(() => {
axios.get("/api/profile").then(/* ... */);
}, []);
}
Ese componente de alto nivel queda acoplado al detalle concreto.
Anti‑patrones y límites prácticos
El anti‑patrón más común es la dependencia directa a infraestructura desde
componentes de UI: imports de SDKs, llamadas HTTP, lógica de serialización o
detalles de caché pegados a la pantalla. Otro olor es el singleton global que funciona
en producción, pero dificulta aislar tests o cambiar implementación. DIP no prohíbe
usar detalles concretos; prohíbe que la política de alto nivel dependa de ellos como
si fueran estables. [46]
La limitación práctica es el sobre‑desacople. Inyectar todo mediante contextos y
providers puede generar árboles profundos de providers, navegación difícil y
abstracciones innecesarias. Cuando la dependencia es local y no necesitas
sustituirla, una prop callback suele ser suficiente. En React, como en el resto del
diseño, DIP da más valor cuando el detalle realmente cambia, se prueba o se
comparte entre pantallas. [47]
Métricas y señales de violación
Las señales más útiles son estas: la pantalla importa directamente clientes
concretos; una sustitución de SDK obliga a tocar componentes de alto nivel; los tests
necesitan mockear módulos o red en vez de inyectar un fake simple; y el grafo de
imports muestra dependencias que suben desde la presentación hacia
infraestructura. React recuerda incluso que, al separar módulos, aparece un árbol de
dependencias de módulos, lo que vuelve natural observar estas direcciones de
acoplamiento. [48]
Comparativa práctica y límites de aplicación
La siguiente tabla resume una lectura operativa de SOLID en React. No sustituye al
análisis de cada caso, pero sirve como guía rápida para decidir qué patrón activar y
qué coste asumir. Todas las recomendaciones están ancladas en la combinación
entre los principios originales y las prácticas oficiales de React moderno. [49]
Principi
o
Patrón
recomendad
o
Ejemplo de API o
estructura
Ventajas
Trade-offs
SRP
Componente
UserCard +
Mejora cohesión,
Más archivos y
Principi
o
Patrón
recomendad
o
de vista +
custom hook
+ wiring
separado
Ejemplo de API o
estructura
useUser(userId) +
UserCardView
Ventajas
testabilidad y
razonamiento
local; reduce
Efectos
innecesarios.
Trade-offs
capas si se
exagera; no
conviene
fragmentar una
UI que cambia
como una sola
unidad. [50]
OCP
Composición List<T>({ items,
renderItem, empty
, children,
slots, render })
props o
hooks
“headless”
Permite extender
comportamiento
sin reescribir el
núcleo; encaja
con composición
sobre herencia.
Riesgo de
sobre‑diseño;
HOCs/render
props tienen
caveats y hoy
muchos casos
migran a hooks.
[51]
LSP
Contratos
conductuale
s pequeños +
pruebas de
sustitución
ButtonLikeProps,
Facilita
intercambiabilida
d real entre
variantes,
adapters y hooks.
TypeScript solo
cubre forma, no
todo el
comportamient
o; accesibilidad
y semántica
requieren
validación extra.
[31]
ISP
Props
estrechas,
contextos
divididos,
hooks por rol
useTasks() /
useTasksDispatch(
)
Reduce
acoplamiento,
mejora claridad y
evita interfaces
“gordas”.
Demasiada
segregación
puede llevar a
provider hell o
APIs
excesivamente
fragmentadas.
[52]
DIP
Provider +
adaptador +
custom
hook; o
callback
props
cuando
basta
ProductsProvider(
{ gateway }) +
useProducts()
Desacopla UI de
infraestructura,
simplifica tests y
cambios de
implementación.
Más indirección
y wiring; no hace
falta un provider
para
dependencias
pequeñas y
locales. [53]
varias
implementaciones
compatibles
Hay cuatro instrumentos que merecen estar en la “caja de herramientas” si quieres
vigilar estos principios en una base React real. Primero, eslint-plugin-reacthooks, porque detecta violaciones de reglas de hooks y dependencias incompletas.
Segundo, reglas generales de complejidad y tamaño como complexity, max-linesper-function y max-lines, que sirven como proxys de SRP. Tercero, StrictMode,
porque fuerza comprobaciones adicionales en desarrollo y revela impuridades o
limpiezas faltantes. Cuarto, React DevTools con su panel de Profiler, porque ayuda a
localizar cuellos de botella y rerenders redundantes que suelen acompañar APIs
demasiado acopladas o abstracciones mal elegidas. [54]
También conviene dejar claro qué no resuelve SOLID en React. No proporciona
umbrales universales para decir “este componente ya es demasiado grande”; las
métricas son señales, no pruebas. No sustituye el buen criterio de DX para decidir si
una abstracción compensa su coste cognitivo. Y, sobre todo, no elimina la necesidad
de pensar la UI como árbol y flujo de datos: React sigue pidiendo separar piezas,
compartir estado en el ancestro adecuado, usar Context con prudencia y reservar los
Efectos para sincronización externa. SOLID funciona bien cuando refuerza ese
modelo mental, y mal cuando lo contradice. [55]
En síntesis, la mejor lectura de “Principios SOLID en React” es esta: SRP para
reagrupar el código por razón de cambio; OCP para abrir puntos de extensión sin
convertir el componente base en una jaula de variantes; LSP para tratar el
comportamiento observable como contrato; ISP para diseñar props, contextos y
hooks pequeños y específicos; y DIP para que la UI dependa de contratos y no de
detalles concretos. Si se combinan con las reglas actuales de React —componentes
funcionales, composición, pureza, uso prudente de Context y custom hooks para
lógica reutilizable—, los cinco principios siguen siendo una guía rigurosa y muy útil en
frontend moderno. [56]
[1] [4] [49] https://principleswiki.net/collections%3Arobert_c._martin_s_principle_collection
https://principles-wiki.net/collections%3Arobert_c._martin_s_principle_collection
[2] [21] [30] https://legacy.reactjs.org/docs/composition-vs-inheritance.html
https://legacy.reactjs.org/docs/composition-vs-inheritance.html
[3] [13] [14] https://es.react.dev/learn/you-might-not-need-an-effect
https://es.react.dev/learn/you-might-not-need-an-effect
[5] [7] [8] [16] [50] https://www.cs.utexas.edu/~downing/papers/SRP-1996.pdf
https://www.cs.utexas.edu/~downing/papers/SRP-1996.pdf
[6] [11] [56] https://react.dev/reference/react/Component
https://react.dev/reference/react/Component
[9] [10] [15] https://react.dev/reference/rules/components-and-hooks-must-be-pure
https://react.dev/reference/rules/components-and-hooks-must-be-pure
[12] https://es.react.dev/learn/sharing-state-between-components
https://es.react.dev/learn/sharing-state-between-components
[17] https://eslint.org/docs/latest/rules/complexity
https://eslint.org/docs/latest/rules/complexity
[18] [20] [24] [25] [51] https://objectmentor.com/resources/articles/ocp.pdf
https://objectmentor.com/resources/articles/ocp.pdf
[19] [22] [23] https://legacy.reactjs.org/docs/higher-order-components.html
https://legacy.reactjs.org/docs/higher-order-components.html
[26] [27] [28] [31] [32] https://www.cs.utexas.edu/~downing/papers/LSP-1996.pdf
https://www.cs.utexas.edu/~downing/papers/LSP-1996.pdf
[29] [33] https://www.typescriptlang.org/docs/handbook/type-compatibility.html
https://www.typescriptlang.org/docs/handbook/type-compatibility.html
[34] [36] [52] https://www.cs.utexas.edu/~downing/papers/ISP-1996.pdf
https://www.cs.utexas.edu/~downing/papers/ISP-1996.pdf
[35] [38] [39] https://es.react.dev/learn/passing-data-deeply-with-context
https://es.react.dev/learn/passing-data-deeply-with-context
[37] [42] [47] https://react.dev/learn/scaling-up-with-reducer-and-context
https://react.dev/learn/scaling-up-with-reducer-and-context
[40] https://react.dev/learn/passing-data-deeply-with-context
https://react.dev/learn/passing-data-deeply-with-context
[41] [43] [45] [46] [53] https://www.cs.utexas.edu/~downing/papers/DIP-1996.pdf
https://www.cs.utexas.edu/~downing/papers/DIP-1996.pdf
[44] https://react.dev/reference/react/createContext
https://react.dev/reference/react/createContext
[48] https://react.dev/learn/understanding-your-ui-as-a-tree
https://react.dev/learn/understanding-your-ui-as-a-tree
[54] https://react.dev/reference/eslint-plugin-react-hooks
https://react.dev/reference/eslint-plugin-react-hooks
[55] https://react.dev/learn/thinking-in-react
https://react.dev/learn/thinking-in-react
0
You can add this document to your study collection(s)
Sign in Available only to authorized usersYou can add this document to your saved list
Sign in Available only to authorized users(For complaints, use another form )