Guía exhaustiva desde los conceptos básicos de React hasta aplicaciones avanzadas con Next.js
React es una biblioteca JavaScript para construir interfaces de usuario. Fue desarrollada por Facebook y se centra en:
# Crear nueva aplicación con Vite (recomendado)
npm create vite@latest mi-app-react --template react
cd mi-app-react
npm install
# Alternativa con Create React App (legacy)
npx create-react-app mi-app-react
cd mi-app-react
import { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Hola Mundo React</h1>
<p>Contador: {count}</p>
<button onClick={() => setCount(count + 1)}>
Incrementar
</button>
</div>
);
}
export default App;
JSX es una extensión de sintaxis que permite escribir HTML en JavaScript:
const element = <h1 className="titulo">Hola, mundo!</h1>;
// Se compila a:
const element = React.createElement(
'h1',
{className: 'titulo'},
'Hola, mundo!'
);
function Saludo({ nombre, edad }) {
return (
<div>
<h2>Hola {nombre}</h2>
<p>Tienes {edad} años</p>
</div>
);
}
// Uso:
<Saludo nombre="Juan" edad={25} />
useState es el hook más básico para manejar estado en componentes funcionales:
import { useState } from 'react';
function Contador() {
const [count, setCount] = useState(0);
const [user, setUser] = useState({ nombre: '', edad: 0 });
return (
<div>
<p>Has hecho clic {count} veces</p>
<button onClick={() => setCount(count + 1)}>
Haz clic
</button>
</div>
);
}
useEffect permite manejar efectos secundarios en componentes funcionales:
import { useState, useEffect } from 'react';
function EjemploAPI() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => setData(data));
}, []); // El array vacío significa que se ejecuta solo al montar
return (
<div>
{data ? <pre>{JSON.stringify(data, null, 2)}</pre> : 'Cargando...'}
</div>
);
}
Formas de manejar formularios en React:
function Formulario() {
const [formData, setFormData] = useState({
nombre: '',
email: '',
});
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
const handleSubmit = (e) => {
e.preventDefault();
console.log(formData);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
name="nombre"
value={formData.nombre}
onChange={handleChange}
/>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
<button type="submit">Enviar</button>
</form>
);
}
npm install formik yup
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';
const validacion = Yup.object().shape({
nombre: Yup.string()
.min(2, 'Demasiado corto')
.required('Requerido'),
email: Yup.string()
.email('Email inválido')
.required('Requerido'),
});
function FormularioAvanzado() {
return (
<Formik
initialValues={{ nombre: '', email: '' }}
validationSchema={validacion}
onSubmit={(values) => {
console.log(values);
}}
>
{({ isSubmitting }) => (
<Form>
<Field type="text" name="nombre" />
<ErrorMessage name="nombre" component="div" />
<Field type="email" name="email" />
<ErrorMessage name="email" component="div" />
<button type="submit" disabled={isSubmitting}>
Enviar
</button>
</Form>
)}
</Formik>
);
}
npm install react-router-dom
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
function App() {
return (
<Router>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/contact">Contact</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="*" element={<h1>404 Not Found</h1>} />
</Routes>
</Router>
);
}
import { createContext, useContext, useState } from 'react';
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = (userData) => {
setUser(userData);
};
const logout = () => {
setUser(null);
};
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
return useContext(AuthContext);
}
import { AuthProvider } from './context/AuthContext';
function App() {
return (
<AuthProvider>
<Router>
{/* Resto de la aplicación */}
</Router>
</AuthProvider>
);
}
import { useAuth } from '../context/AuthContext';
function Login() {
const { login } = useAuth();
const handleLogin = () => {
login({ name: 'Usuario', email: 'usuario@example.com' });
};
return (
<button onClick={handleLogin}>
Iniciar sesión
</button>
);
}
Creando hooks personalizados para reutilizar lógica:
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const storedValue = localStorage.getItem(key);
return storedValue ? JSON.parse(storedValue) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
export default useLocalStorage;
import useLocalStorage from '../hooks/useLocalStorage';
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
return (
<button onClick={toggleTheme}>
Cambiar a tema {theme === 'light' ? 'oscuro' : 'claro'}
</button>
);
}
npm install @reduxjs/toolkit react-redux
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
import { createSlice } from '@reduxjs/toolkit';
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0,
},
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
import { Provider } from 'react-redux';
import { store } from './app/store';
function App() {
return (
<Provider store={store}>
<Router>
{/* Resto de la aplicación */}
</Router>
</Provider>
);
}
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from '../features/counter/counterSlice';
function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<button onClick={() => dispatch(decrement())}>-</button>
<span>{count}</span>
<button onClick={() => dispatch(increment())}>+</button>
</div>
);
}
npm install zustand
import { create } from 'zustand';
const useCounterStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
export default useCounterStore;
import useCounterStore from '../store/useCounterStore';
function CounterZustand() {
const { count, increment, decrement } = useCounterStore();
return (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
);
}
import { memo } from 'react';
const ListItem = memo(function ListItem({ item }) {
console.log('Renderizando item:', item.id);
return <li>{item.name}</li>;
});
function List({ items }) {
return (
<ul>
{items.map(item => (
<ListItem key={item.id} item={item} />
))}
</ul>
);
}
import { useMemo } from 'react';
function ExpensiveComponent({ list }) {
const sortedList = useMemo(() => {
console.log('Ordenando lista...');
return [...list].sort((a, b) => a.value - b.value);
}, [list]);
return <div>{/* Renderizar con sortedList */}</div>;
}
import { useCallback, useState } from 'react';
function ParentComponent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('Count:', count);
}, [count]);
return <ChildComponent onClick={handleClick} />;
}
const ChildComponent = memo(function ChildComponent({ onClick }) {
console.log('Renderizando ChildComponent');
return <button onClick={onClick}>Haz clic</button>;
});
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/setupTests.js',
},
});
import '@testing-library/jest-dom';
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from '../Counter';
describe('Counter', () => {
it('should increment counter when + button is clicked', () => {
render(<Counter />);
const incrementButton = screen.getByText('+');
const countDisplay = screen.getByText('0');
fireEvent.click(incrementButton);
expect(countDisplay).toHaveTextContent('1');
});
});
npm install -D typescript @types/react @types/react-dom
import { useState } from 'react';
interface User {
id: number;
name: string;
email: string;
}
interface Props {
initialUsers: User[];
}
function TypedComponent({ initialUsers }: Props) {
const [users, setUsers] = useState<User[]>(initialUsers);
const [newUser, setNewUser] = useState<Omit<User, 'id'>>({ name: '', email: '' });
const addUser = () => {
setUsers([...users, { ...newUser, id: Date.now() }]);
setNewUser({ name: '', email: '' });
};
return (
<div>
<ul>
{users.map(user => (
<li key={user.id}>{user.name} ({user.email})</li>
))}
</ul>
<input
type="text"
value={newUser.name}
onChange={(e) => setNewUser({ ...newUser, name: e.target.value })}
placeholder="Nombre"
/>
<input
type="email"
value={newUser.email}
onChange={(e) => setNewUser({ ...newUser, email: e.target.value })}
placeholder="Email"
/>
<button onClick={addUser}>Agregar Usuario</button>
</div>
);
}
export default TypedComponent;