Guia practica de React 18 y Redux Toolkit actualizada
Frontend

Guia practica de React 18 y Redux Toolkit actualizada

3 October 2026 · 13 min de lectura
Home / Frontend / Guia practica de React 18 y Redux Toolkit actualizada
Categorías:

Introduccion a React y la gestion de estado moderna

React se ha consolidado como una de las bibliotecas mas utilizadas para construir interfaces de usuario interactivas. Su modelo basado en componentes, el uso de un DOM virtual y el flujo de datos unidireccional permiten crear aplicaciones escalables y mantenibles. En 2026 React 19 es la version activa principal, mientras que React 18 permanece en modo de soporte de seguridad. Aun asi, los conceptos fundamentales introducidos en React 18 siguen siendo la base sobre la que se construyen las aplicaciones actuales, incluyendo concurrent rendering, automatic batching y los hooks principales.

Redux Toolkit representa la forma recomendada de trabajar con Redux en la actualidad. Elimina gran parte del codigo repetitivo que caracterizaba a las implementaciones clasicas y ofrece utilidades como createSlice, configureStore y RTK Query. Estas herramientas facilitan la gestion de estado global de forma predecible y con buenas practicas integradas por defecto.

Este articulo ofrece una vision practica y actualizada de los elementos esenciales para construir aplicaciones con React y Redux Toolkit. Se cubren desde la estructura de un proyecto y la creacion de componentes hasta el manejo de estado local con hooks, el uso de Context API y la implementacion de un store global con Redux Toolkit. Todos los ejemplos se presentan en JavaScript moderno y pueden adaptarse facilmente a TypeScript.

Configuracion inicial de un proyecto React

La forma mas comun de iniciar un proyecto React en 2026 es mediante Vite, que ofrece un entorno de desarrollo rapido y una configuracion minima. Aunque Create React App se encuentra en modo de mantenimiento, muchas bases de codigo legacy aun lo utilizan. Para un proyecto nuevo se recomienda Vite junto con la plantilla oficial de Redux Toolkit cuando se planea usar gestion de estado global.

Una vez creado el proyecto, la estructura tipica incluye una carpeta src con los componentes, una carpeta features para organizar la logica por dominio y un archivo de configuracion del store. Es habitual definir un archivo de ajustes del editor para mantener consistencia en el formato del codigo.

El siguiente comando crea un proyecto basico con Vite:

npm create vite@latest mi-app -- --template react
cd mi-app
npm install
npm run dev

Despues de la instalacion se puede anadir Redux Toolkit y React-Redux:

npm install @reduxjs/toolkit react-redux

La organizacion por caracteristicas (feature-based) es preferible a la organizacion por tipo de archivo. Cada feature contiene su slice, sus componentes y sus selectores, lo que facilita la escalabilidad cuando la aplicacion crece.

Componentes y la sintaxis JSX

Un componente de React es una funcion o clase que devuelve elementos que describen lo que debe aparecer en la interfaz. En la practica moderna se utilizan casi exclusivamente componentes funcionales. JSX permite escribir una sintaxis similar a HTML dentro de JavaScript, que luego se transforma en llamadas a React.createElement.

Un componente simple se define de la siguiente manera:

function Saludo() {
    return <h1>Hola desde un componente React</h1>;
}

Los componentes pueden anidarse. Un componente padre puede incluir otros componentes hijos, formando un arbol de elementos. Esta composicion es una de las caracteristicas mas potentes de React, ya que permite reutilizar piezas de interfaz en distintos contextos.

JSX admite expresiones de JavaScript entre llaves. Es posible interpolar variables, llamar a funciones o evaluar condiciones. Las clases CSS se asignan mediante la propiedad className, ya que class es una palabra reservada en JavaScript.

Cuando se trabaja con listas de elementos, cada elemento debe recibir una prop key unica. Esta clave ayuda a React a identificar que elementos han cambiado, se han anadido o se han eliminado, optimizando el proceso de reconciliacion.

function ListaLibros({ libros }) {
    return (
        <ul>
            {libros.map((libro) => (
                <li key={libro.id}>{libro.titulo}</li>
            ))}
        </ul>
    );
}

La prop key no se transmite al componente hijo; se utiliza internamente por React. Utilizar el indice del array como key solo es aceptable cuando la lista es estatica y no se reordena.

Props y el flujo de datos

Las props son el mecanismo principal para pasar datos desde un componente padre hacia sus hijos. Son de solo lectura: el componente que las recibe no debe modificarlas. Esta inmutabilidad refuerza el flujo de datos unidireccional que caracteriza a React.

Cuando se necesita pasar contenido arbitrario, se utiliza la prop children. Cualquier elemento colocado entre las etiquetas de apertura y cierre de un componente se recibe como children.

function Tarjeta({ titulo, children }) {
    return (
        <div className="tarjeta">
            <h2>{titulo}</h2>
            <div className="contenido">{children}</div>
        </div>
    );
}

// Uso
<Tarjeta titulo="Detalle del producto">
    <p>Descripcion completa del producto seleccionado.</p>
</Tarjeta>;

Las props pueden ser de cualquier tipo: cadenas, numeros, booleanos, objetos, arrays o funciones. Cuando se pasan funciones, estas suelen utilizarse como manejadores de eventos que el hijo invoca para comunicar cambios al padre.

En aplicaciones mas complejas es frecuente elevar el estado (lifting state up). El estado se mantiene en el ancestro comun mas cercano y se distribuye hacia abajo mediante props. Este patron evita la necesidad de herramientas de estado global cuando el arbol de componentes no es demasiado profundo.

Eventos y formularios controlados

React utiliza un sistema de eventos sinteticos que normaliza el comportamiento entre navegadores. Los manejadores de eventos se pasan como props con nombres en camelCase, como onClick, onChange o onSubmit.

Los formularios controlados son el patron recomendado. El valor de cada campo se almacena en el estado del componente y se actualiza mediante un manejador onChange. De esta forma React se convierte en la unica fuente de verdad del valor del input.

import { useState } from "react";

function FormularioContacto() {
    const [nombre, setNombre] = useState("");
    const [email, setEmail] = useState("");

    const handleSubmit = (e) => {
        e.preventDefault();
        console.log({ nombre, email });
    };

    return (
        <form onSubmit={handleSubmit}>
            <input
                type="text"
                value={nombre}
                onChange={(e) => setNombre(e.target.value)}
                placeholder="Nombre"
            />
            <input
                type="email"
                value={email}
                onChange={(e) => setEmail(e.target.value)}
                placeholder="Email"
            />
            <button type="submit">Enviar</button>
        </form>
    );
}

Evitar el comportamiento por defecto del formulario con preventDefault es esencial para controlar el flujo de la aplicacion. En formularios mas elaborados se pueden validar los campos antes de enviar y mostrar mensajes de error asociados a cada input.

Hooks fundamentales useState y useEffect

El hook useState permite anadir estado local a un componente funcional. Devuelve un par formado por el valor actual y una funcion para actualizarlo. Cada vez que se llama a la funcion de actualizacion, React vuelve a renderizar el componente con el nuevo valor.

const [contador, setContador] = useState(0);

const incrementar = () => setContador((prev) => prev + 1);

Es recomendable utilizar la forma funcional de actualizacion cuando el nuevo valor depende del valor anterior, ya que garantiza que se trabaja con el estado mas reciente.

El hook useEffect se utiliza para ejecutar efectos secundarios: peticiones a APIs, suscripciones, manipulacion directa del DOM o temporizadores. Recibe una funcion de efecto y un array de dependencias. Si el array esta vacio, el efecto se ejecuta solo una vez despues del montaje. Si se omiten las dependencias, el efecto se ejecuta despues de cada renderizado, lo que suele ser indeseable.

import { useState, useEffect } from "react";

function PerfilUsuario({ userId }) {
    const [usuario, setUsuario] = useState(null);

    useEffect(() => {
        let cancelado = false;

        async function cargarUsuario() {
            const respuesta = await fetch(`/api/usuarios/${userId}`);
            const datos = await respuesta.json();
            if (!cancelado) {
                setUsuario(datos);
            }
        }

        cargarUsuario();

        return () => {
            cancelado = true;
        };
    }, [userId]);

    if (!usuario) return <p>Cargando...</p>;
    return <h1>{usuario.nombre}</h1>;
}

La funcion de limpieza que se devuelve desde useEffect se ejecuta cuando el componente se desmonta o antes de que el efecto se vuelva a ejecutar. Es el lugar adecuado para cancelar peticiones o eliminar suscripciones.

useRef, hooks personalizados y Context API

useRef devuelve un objeto mutable cuya propiedad current persiste entre renderizados. Se utiliza principalmente para acceder a elementos del DOM o para almacenar valores que no deben provocar un nuevo renderizado cuando cambian.

import { useRef, useEffect } from "react";

function InputConFoco() {
    const inputRef = useRef(null);

    useEffect(() => {
        inputRef.current.focus();
    }, []);

    return <input ref={inputRef} type="text" />;
}

Los hooks personalizados permiten extraer logica reutilizable. Un hook personalizado es simplemente una funcion cuyo nombre comienza por use y que puede llamar a otros hooks. Esta tecnica reduce la duplicacion de codigo y mejora la legibilidad.

function useLocalStorage(clave, valorInicial) {
    const [valor, setValor] = useState(() => {
        const guardado = localStorage.getItem(clave);
        return guardado !== null ? JSON.parse(guardado) : valorInicial;
    });

    useEffect(() => {
        localStorage.setItem(clave, JSON.stringify(valor));
    }, [clave, valor]);

    return [valor, setValor];
}

Context API ofrece una forma de compartir datos a traves del arbol de componentes sin necesidad de pasar props manualmente en cada nivel. Se crea un contexto con createContext, se proporciona un valor mediante un Provider y se consume con useContext.

import { createContext, useContext, useState } from "react";

const TemaContext = createContext();

function TemaProvider({ children }) {
    const [tema, setTema] = useState("claro");
    return (
        <TemaContext.Provider value={{ tema, setTema }}>
            {children}
        </TemaContext.Provider>
    );
}

function BotonTema() {
    const { tema, setTema } = useContext(TemaContext);
    return (
        <button onClick={() => setTema(tema === "claro" ? "oscuro" : "claro")}>
            Cambiar a tema {tema === "claro" ? "oscuro" : "claro"}
        </button>
    );
}

Context es adecuado para datos que cambian con poca frecuencia, como el tema, el idioma o la autenticacion. Para estado que se actualiza con alta frecuencia o que requiere logica compleja, Redux Toolkit suele ser una mejor opcion.

useReducer para logica de estado compleja

Cuando el estado de un componente involucra multiples subvalores o cuando la proxima actualizacion depende del estado anterior de forma elaborada, useReducer ofrece una alternativa mas predecible que multiples llamadas a useState.

import { useReducer } from "react";

const estadoInicial = { contador: 0 };

function reducer(estado, accion) {
    switch (accion.type) {
        case "incrementar":
            return { contador: estado.contador + 1 };
        case "decrementar":
            return { contador: estado.contador - 1 };
        case "reset":
            return estadoInicial;
        default:
            throw new Error(`Accion desconocida: ${accion.type}`);
    }
}

function Contador() {
    const [estado, dispatch] = useReducer(reducer, estadoInicial);

    return (
        <div>
            <p>Contador: {estado.contador}</p>
            <button onClick={() => dispatch({ type: "incrementar" })}>+</button>
            <button onClick={() => dispatch({ type: "decrementar" })}>-</button>
            <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
        </div>
    );
}

Este patron es el precursor conceptual de los slices de Redux Toolkit. La funcion reductora es pura: recibe el estado actual y una accion, y devuelve el nuevo estado sin efectos secundarios.

Introduccion a Redux Toolkit

Redux Toolkit es el conjunto de herramientas oficial recomendado para escribir logica de Redux. Incluye configureStore, createSlice, createAsyncThunk y RTK Query. Estas utilidades reducen significativamente el codigo repetitivo y aplican buenas practicas por defecto, como la inmutabilidad mediante Immer y la comprobacion de serializacion en modo desarrollo.

La configuracion basica de un store se realiza con configureStore:

import { configureStore } from "@reduxjs/toolkit";
import contadorReducer from "./features/contador/contadorSlice";

export const store = configureStore({
    reducer: {
        contador: contadorReducer,
    },
});

El store se proporciona a la aplicacion mediante el componente Provider de React-Redux:

import { Provider } from "react-redux";
import { store } from "./store";
import App from "./App";

root.render(
    <Provider store={store}>
        <App />
    </Provider>
);

Creacion de slices con createSlice

createSlice genera automaticamente action creators y action types a partir de un conjunto de funciones reductoras. Internamente utiliza Immer, por lo que es posible escribir actualizaciones que parecen mutaciones, aunque el estado resultante es inmutable.

import { createSlice } from "@reduxjs/toolkit";

const contadorSlice = createSlice({
    name: "contador",
    initialState: { valor: 0 },
    reducers: {
        incrementar: (state) => {
            state.valor += 1;
        },
        decrementar: (state) => {
            state.valor -= 1;
        },
        incrementarPorCantidad: (state, action) => {
            state.valor += action.payload;
        },
    },
});

export const { incrementar, decrementar, incrementarPorCantidad } =
    contadorSlice.actions;
export default contadorSlice.reducer;

En el componente se utilizan los hooks useSelector y useDispatch (o sus versiones tipadas en proyectos TypeScript):

import { useSelector, useDispatch } from "react-redux";
import { incrementar, decrementar } from "./contadorSlice";

function ContadorRedux() {
    const valor = useSelector((state) => state.contador.valor);
    const dispatch = useDispatch();

    return (
        <div>
            <p>{valor}</p>
            <button onClick={() => dispatch(incrementar())}>+</button>
            <button onClick={() => dispatch(decrementar())}>-</button>
        </div>
    );
}

La organizacion por slices permite mantener la logica de cada dominio de la aplicacion encapsulada y facil de localizar.

Logica asincrona con createAsyncThunk y RTK Query

createAsyncThunk simplifica la creacion de acciones asincronas. Genera automaticamente acciones para los estados pending, fulfilled y rejected, que pueden manejarse dentro del slice mediante extraReducers.

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";

export const obtenerUsuarios = createAsyncThunk(
    "usuarios/obtener",
    async () => {
        const respuesta = await fetch("/api/usuarios");
        return respuesta.json();
    }
);

const usuariosSlice = createSlice({
    name: "usuarios",
    initialState: { lista: [], estado: "idle", error: null },
    reducers: {},
    extraReducers: (builder) => {
        builder
            .addCase(obtenerUsuarios.pending, (state) => {
                state.estado = "loading";
            })
            .addCase(obtenerUsuarios.fulfilled, (state, action) => {
                state.estado = "succeeded";
                state.lista = action.payload;
            })
            .addCase(obtenerUsuarios.rejected, (state, action) => {
                state.estado = "failed";
                state.error = action.error.message;
            });
    },
});

Para la mayoria de los casos de obtencion de datos desde un servidor, RTK Query ofrece una solucion aun mas completa. Define endpoints, genera hooks automaticos, gestiona la cache, los estados de carga y la invalidacion de datos.

import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

export const api = createApi({
    baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
    endpoints: (builder) => ({
        getUsuarios: builder.query({
            query: () => "usuarios",
        }),
        getUsuarioPorId: builder.query({
            query: (id) => `usuarios/${id}`,
        }),
    }),
});

export const { useGetUsuariosQuery, useGetUsuarioPorIdQuery } = api;

El uso del hook generado es directo:

function ListaUsuarios() {
    const { data, error, isLoading } = useGetUsuariosQuery();

    if (isLoading) return <p>Cargando...</p>;
    if (error) return <p>Error al cargar los datos</p>;

    return (
        <ul>
            {data.map((usuario) => (
                <li key={usuario.id}>{usuario.nombre}</li>
            ))}
        </ul>
    );
}

RTK Query reduce drásticamente la cantidad de codigo necesario para gestionar el ciclo de vida de las peticiones y mantiene la cache de forma automatica.

Consideraciones de rendimiento

React 18 introdujo automatic batching, que agrupa multiples actualizaciones de estado en un unico renderizado incluso dentro de promesas, timeouts o manejadores nativos de eventos. Esta mejora reduce el numero de renderizados innecesarios sin requerir cambios en el codigo de la aplicacion.

Para optimizar renderizados selectivos se pueden utilizar React.memo, useMemo y useCallback. Sin embargo, en 2026 el React Compiler (disponible en versiones recientes) realiza muchas de estas optimizaciones de forma automatica, por lo que el uso manual de memoizacion debe reservarse para casos medidos y justificados.

Cuando se trabaja con listas grandes, es recomendable implementar virtualizacion o paginacion. Librerias como react-window permiten renderizar solo los elementos visibles, manteniendo un rendimiento aceptable incluso con miles de filas.

En el contexto de Redux, los selectores memorizados con createSelector evitan recálculos innecesarios cuando el estado relevante no ha cambiado. RTK Query ya incluye mecanismos de cache que minimizan las peticiones redundantes.

Estructura de carpetas recomendada

Una organizacion clara facilita el mantenimiento a medida que el proyecto crece. Una estructura habitual es la siguiente:

src/
├── app/
│   ├── store.js
│   └── hooks.js
├── features/
│   ├── contador/
│   │   ├── contadorSlice.js
│   │   └── Contador.js
│   └── usuarios/
│       ├── usuariosSlice.js
│       └── ListaUsuarios.js
├── components/
│   └── ui/
├── hooks/
└── App.js

Los hooks tipados (useAppDispatch y useAppSelector) se definen una sola vez y se reutilizan en toda la aplicacion, evitando importar los tipos del store en cada archivo.

Conclusiones

React y Redux Toolkit forman una combinacion solida para el desarrollo de interfaces modernas. Los componentes funcionales, los hooks y el flujo de datos unidireccional proporcionan una base predecible, mientras que Redux Toolkit elimina gran parte de la complejidad historica asociada a la gestion de estado global.

En 2026 es recomendable comenzar los proyectos nuevos con las versiones mas recientes de React y adoptar Redux Toolkit cuando se necesite un store centralizado, auditoria de acciones o integracion con RTK Query. Para aplicaciones mas pequenas, Context API o soluciones ligeras pueden ser suficientes. La clave reside en elegir la herramienta adecuada segun la complejidad real del estado y los requisitos del equipo.

Dominar estos fundamentos permite construir aplicaciones mantenibles, escalables y preparadas para evolucionar con las mejoras continuas del ecosistema React.