Interstellar Code

Principios SOLID en React

En este artículo vamos a explorar cómo podemos aplicar los principios SOLID en el desarrollo de aplicaciones mediante el uso de la librería React. Cada principio será ilustrado con ejemplos prácticos para facilitar su comprensión y aplicación en proyectos reales, con la intención de mejorar la calidad, mantenibilidad y escalabilidad del código.

28 de agosto de 2025

Tabla de contenido:

  1. 1. Principios SOLID en React
  2. 2. Single Responsibility Principle (SRP)
  3. 3. Open/Closed Principle (OCP)
  4. 4. Liskov Substitution Principle (LSP)
  5. 5. Interface Segregation Principle (ISP)
  6. 6. Dependency Inversion Principle (DIP)

Principios SOLID en React

Los principios SOLID son un conjunto de cinco principios de diseño de software orientado a objetos que ayudan a crear sistemas más mantenibles, escalables y robustos. Estos principios son en particular útiles en el desarrollo de aplicaciones con React, ya que fomentan la creación de componentes y módulos que son fáciles de entender y modificar.

En general dentro de la arquitectura de software, los principios SOLID son considerados buenas prácticas para el diseño y desarrollo de software. Estos principios fueron introducidos por Robert C. Martin (también conocido como “Uncle Bob”) y son ampliamente utilizados en la industria del desarrollo de software para mejorar la calidad del código y facilitar su mantenimiento a largo plazo.


Single Responsibility Principle (SRP)

El principio de responsabilidad única establece que un módulo o clase debe tener una única razón para cambiar, es decir, debe estar enfocado en una sola tarea o responsabilidad. En el contexto de React, esto significa que cada componente debe tener una única responsabilidad y no debe manejar múltiples tareas o lógicas complejas. Al igual que el uso de servicios, estados globales o hooks personalizados para separar las responsabilidades.

El siguiente ejemplo muestra cómo aplicar el principio de responsabilidad única en un componente de React que obtiene y muestra una lista de tareas (todos) desde una API. En este caso, hemos separado la lógica de obtención de datos en un hook personalizado (useFetchTodos) y la lógica de presentación en el componente principal (SingleResponsibility).

index.tsx: Solo se encarga mostrar la información de las tareas.

import { useFetchTodos } from './hooks/useFetchTodos'

export default function SingleResponsibility() {
  const { todos, isFetching } = useFetchTodos()

  if (isFetching) {
    return <p>Loading...</p>
  }

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          <span>{todo.id}</span>
          <span>{todo.title}</span>
        </li>
      ))}
    </ul>
  )
}

types/todos.d.ts: Solo define el tipo de dato.

export type TodoType = {
  id: number
  userId: number
  title: string
  completed: boolean
}

services/todos.ts: Solo se encarga de obtener los datos.

import axios from 'axios'
import type { TodoType } from '../types/todos'

export async function fetchTodos() {
  try {
    const res = await axios.get<TodoType[]>('https://jsonplaceholder.typicode.com/todos')
    return res.data
  } catch (err) {
    if ((err as { code: number }).code === 404) {
      throw new Error('Not Found')
    }

    return []
  }
}

hooks/useFetchTodos.ts: Es un hook personalizado que solo se encarga de manejar el estado y la lógica de obtención de datos.

import { useState, useEffect } from 'react'
import { fetchTodos } from '../services/todos'
import type { TodoType } from '../types/todos'

export function useFetchTodos() {
  const [todos, setTodos] = useState<TodoType[]>([])
  const [isFetching, setIsFetching] = useState(true)

  useEffect(() => {
    fetchTodos()
      .then((todos) => setTodos(todos))
      .finally(() => setIsFetching(false))
  }, [])

  return {
    todos,
    isFetching,
  }
}

Open/Closed Principle (OCP)

El principio de abierto/cerrado establece que una entidad de software (como una clase, módulo o función) debe estar abierta para la extensión pero cerrada para la modificación. Esto significa que deberíamos poder agregar nuevas funcionalidades sin tener que modificar el código existente, lo que ayuda a evitar errores y mantener la estabilidad del sistema. En React, esto se puede lograr mediante el uso de componentes reutilizables y la composición de componentes, permitiendo que los componentes existentes se extiendan sin necesidad de cambiar su implementación interna.

index.tsx: Componente principal que utiliza otros componentes especializados para diferentes tipos de títulos.

import { Fragment } from 'react'
import { TitleWithLink } from './components/TitleWithLink'
import { TitleWithButton } from './components/TitleWithButton'
import { TitleWithEmoji } from './components/TitleWithEmoji'

export default function OpenClosed() {
  return (
    <Fragment>
      <TitleWithLink
        title='Link Button'
        buttonText='Aloha!'
        href='https://www.midu.dev'
      />

      <TitleWithButton
        title='Normal Button'
        buttonText='Aloha!'
        onClick={() => console.log('Aloha!')}
      />

      <TitleWithEmoji
        title='Emoji Title'
        emoji='✌'
      />
    </Fragment>
  )
}

types/buttons.d.ts: Define los tipos de datos para los diferentes componentes de título.

import React from 'react'

export type Props = {
  title: string
  type: 'default' | 'withLinkButton' | 'withNormalButton'
  href?: string
  buttonText?: string
  onClick?: () => void
}

export type TitleProps = {
  title: string
  children?: React.ReactElement
}

export type TitleWithLinkProps = {
  title: string
  href?: string
  buttonText?: string
}

export type TitleWithButtonProps = {
  title: string
  buttonText?: string
  onClick?: () => void
}

export type TitleWithEmojiProps = {
  title: string
  emoji?: string
}

components/Title.tsx: Componente base que maneja la presentación del título y cualquier contenido adicional.

import type { TitleProps } from '../types/buttons.d'

export function Title({ title, children }: TitleProps) {
  return (
    <div 
      style={{ 
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
        gap: '1rem',
      }}
    >
      <h1>{title}</h1>
      {children}
    </div>
  )
}

components/TitleWithButton.tsx: Componente especializado que extiende la funcionalidad del componente base para incluir un botón.

import { Title } from './Title'
import type { TitleWithButtonProps } from '../types/buttons.d'

export function TitleWithButton({ title, buttonText, onClick }: TitleWithButtonProps) {
  return (
    <Title title={title}>
      <button onClick={onClick}>{buttonText}</button>
    </Title>
  )
}

components/TitleWithEmoji.tsx: Componente especializado que extiende la funcionalidad del componente base para incluir un emoji.

import { Title } from './Title'
import type { TitleWithEmojiProps } from '../types/buttons.d'

export function TitleWithEmoji({ title, emoji }: TitleWithEmojiProps) {
  return (
    <Title title={title}>
      <p style={{ fontSize: '48px' }}>{emoji}</p>
    </Title>
  )
}

components/TitleWithLink.tsx: Componente especializado que extiende la funcionalidad del componente base para incluir un enlace.

import { Title } from './Title'
import type { TitleWithLinkProps } from '../types/buttons.d'

export function TitleWithLink({ title, href, buttonText }: TitleWithLinkProps) {
  return (
    <Title title={title}>
      <div>
        <a href={href}>{buttonText}</a>
      </div>
    </Title>
  )
}

Liskov Substitution Principle (LSP)

El principio de sustitución de Liskov establece que los objetos de una clase derivada deben poder sustituir a los objetos de la clase base sin alterar el correcto funcionamiento del programa. En el contexto de React, esto significa que los componentes derivados o especializados deben poder ser utilizados en lugar de sus componentes base sin causar errores o comportamientos inesperados. Esto se puede lograr mediante la creación de componentes que extienden la funcionalidad de un componente base, asegurando que mantengan la misma interfaz y comportamiento esperado.

index.tsx: Componente principal que utiliza botones de diferentes colores, todos derivados del mismo componente base.

import React from 'react'

const ButtonSizes = {
  sm: '16px',
  md: '24px',
  lg: '32px',
  xl: '40px',
} as const

type ButtonProps = {
  children: React.ReactNode
  color?: string
  size: keyof typeof ButtonSizes
}

function Button({ children, color = 'black', size }: ButtonProps) {
  const buttonSize = ButtonSizes[size]

  return (
    <button style={{ backgroundColor: color, fontSize: buttonSize }}>
      {children}
    </button>
  )
}

type ColorizedButtonsProps = Omit<ButtonProps, 'color'>

function RedButton({ children, size }: ColorizedButtonsProps) {
  return <Button color='red' size={size}>{children}</Button>
}

function GreenButton({ children, size }: ColorizedButtonsProps) {
  return <Button color='green' size={size}>{children}</Button>
}

function BlueButton({ children, size }: ColorizedButtonsProps) {
  return <Button color='blue' size={size}>{children}</Button>
}

export default function LiskovSubstitution() {
  return (
    <div>
      <RedButton size='md'>Rojo</RedButton>
      <GreenButton size='md'>Verde</GreenButton>
      <BlueButton size='md'>Azul</BlueButton>
      <Button size='md'>Default</Button>
    </div>
  )
}

Interface Segregation Principle (ISP)

El principio de segregación de interfaces establece que los clientes no deben verse obligados a depender de interfaces que no utilizan. En el contexto de React, esto significa que los componentes deben tener interfaces específicas y enfocadas, evitando la creación de componentes monolíticos con múltiples responsabilidades. Al diseñar componentes con interfaces claras y específicas, se facilita su reutilización y mantenimiento.

index.tsx: Componente principal que utiliza componentes especializados para mostrar diferentes partes de una publicación (post).

type PostType = {
  title: string
  author: {
    name: string
    age: number
  },
  createdAt: Date
}

function Post({ post }: { post: PostType }) {
  return (
    <div>
      <PostTitle title={post.title} />
      <span>{post.author.name}</span>
      <PostData createdAt={post.createdAt} />
    </div>
  )
}

type TitleProps = {
  title: string
}

function PostTitle({ title }: TitleProps) {
  return <h1>{title}</h1>
}

type DataProps = {
  createdAt: Date
}

function PostData({ createdAt }: DataProps) {
  return <time>{createdAt.toISOString()}</time>
}

export default function InterfaceSegregation() {
  return (
    <Post
      post={{
        title: 'Curso de React',
        author: {
          name: 'Miguel Ángel Duran',
          age: 38,
        },
        createdAt: new Date(),
      }}
    />
  )
}

Dependency Inversion Principle (DIP)

El principio de inversión de dependencias establece que los módulos de alto nivel no deben depender de módulos de bajo nivel, sino que ambos deben depender de abstracciones. En el contexto de React, esto significa que los componentes de alto nivel (como los componentes de presentación) no deben depender directamente de detalles específicos (como la forma en que se obtienen los datos), sino que deben depender de abstracciones (como hooks personalizados o servicios) que puedan ser fácilmente reemplazados o modificados sin afectar a los componentes de alto nivel.

index.tsx: Componente principal que utiliza un hook personalizado para obtener datos, permitiendo cambiar la fuente de datos sin modificar el componente.

import { useData } from './hooks/useData'
import type { TodoType } from './types/todos.d'
import { fetcherFromApi as fetcher } from './utils/fetchers'

export default function DependencyInversion() {
  const { data } = useData<TodoType[]>({ 
    key: '/todos', 
    fetcher, 
  })

  if (!data) return (
    <p>Loading ....</p>
  )

  return (
    <ul>
      {data.map((todo) => (
        <li key={todo.id}>
          <span>{todo.id}</span>
          <span>{todo.title}</span>
        </li>
      ))}
    </ul>
  )
}

types/todos.d.ts: Define el tipo de dato.

export type TodoType = {
  id: number
  userId: number
  title: string
  completed: boolean
}

constants/todos.ts: Define la constante con la URL de la API.

export const TODOS_API_URL = 'https://jsonplaceholder.typicode.com/todos'

utils/fetchers.ts: Define diferentes fetchers para obtener datos desde distintas fuentes (API, localStorage, mocks).

import { TODOS_API_URL } from '../constants/todos'

export const fetcherFromApi = async function<T>(): Promise<T> {
  const url = TODOS_API_URL
  const res = await fetch(url)
  return res.json()
}

export const fetcherFromLocalStorage = async function<T>(): Promise<T> {
  const todos = localStorage.getItem('todos')
  return todos ? JSON.parse(todos) : []
}

export const fetcherFromMocks = async function<T>(): Promise<T> {
  return [
    {
      id: 1,
      userId: 1,
      title: 'Todo 1',
      completed: false,
    },
    {
      id: 2,
      userId: 1,
      title: 'Todo 2',
      completed: true,
    },
  ] as T
}

hooks/useData.ts: Hook personalizado que utiliza SWR para obtener datos, dependiendo de la abstracción del fetcher proporcionado.

import useSWR from 'swr'

interface UseData<T> {
  key: string
  fetcher: () => Promise<T>
}

interface Response<T> {
  data: T | undefined
  error: string | undefined
  isValidating: boolean
}

export function useData<T>({ key, fetcher }: UseData<T>): Response<T> {
  const { data, error, isValidating } = useSWR<T, string>(key, fetcher)
  return { data, error, isValidating }
}