Patrones React
Patrones reutilizables para construir interfaces flexibles y mantenibles en React.
🧩 Componentes Compuestos (Compound Components)
Build components that talk to each other like best friends! 🤝 Compound Components let you create flexible, composable APIs where parent and children share state seamlessly through Context. It's like LEGO blocks for your UI! 🧱
🟠 Impact: HIGH — The secret sauce behind every great component library. Master this and you'll build APIs that developers actually enjoy using! 🎯
📋 In this section: Context-based state sharing • Composable sub-components • Flexible API design • Real-world Accordion example
Qué observar
- Un solo componente con todas las props; estructura rígida.
// ❌ WRONG: Single component with all props - not flexibleconst faqItems = [{ question: "What is React?", answer: "A UI library for building interfaces." },{ question: "Why use it?", answer: "Component-based, reusable, and declarative." },];function Accordion({items = [],titleKey,contentKey,defaultOpen = 0}) {const [openIndex, setOpenIndex] = useState(defaultOpen);return (<div>{items.map((item, index) => (<div key={index}><button onClick={() => setOpenIndex(index)}>{item[titleKey]}</button>{openIndex === index && (<div>{item[contentKey]}</div>)}</div>))}</div>);}// Usage - rigid structure (faqItems must match titleKey/contentKey)function App() {return (<Accordionitems={faqItems}titleKey="question"contentKey="answer"defaultOpen={0}/>);}export default App;// Problems:// - Can't customize individual items easily// - Can't rearrange header/content order// - Hard to add custom styling per item// - Limited flexibility for different use cases
Qué observar
- Componentes compuestos con contexto; estructura flexible.
// ✅ GOOD: Compound components - flexible and composableimport { createContext, useContext, useState } from 'react';const AccordionContext = createContext<{openIndex: number;setOpenIndex: (index: number) => void;} | null>(null);function Accordion({ children, defaultOpen = 0 }) {const [openIndex, setOpenIndex] = useState(defaultOpen);return (<AccordionContext.Provider value={{ openIndex, setOpenIndex }}><div className="accordion">{children}</div></AccordionContext.Provider>);}function AccordionItem({ index, children }) {const context = useContext(AccordionContext);if (!context) throw new Error('AccordionItem must be inside Accordion');const { openIndex, setOpenIndex } = context;const isOpen = openIndex === index;return (<div className="accordion-item">{React.Children.map(children, child =>React.cloneElement(child, { isOpen, onToggle: () => setOpenIndex(index) }))}</div>);}function AccordionHeader({ children, isOpen, onToggle }) {return (<buttononClick={onToggle}className={`accordion-header ${isOpen ? 'open' : ''}`}>{children}</button>);}function AccordionContent({ children, isOpen }) {if (!isOpen) return null;return <div className="accordion-content">{children}</div>;}Accordion.Item = AccordionItem;Accordion.Header = AccordionHeader;Accordion.Content = AccordionContent;function Icon() { return <span>📌 </span>; }function CustomComponent() { return <p>Custom content here</p>; }function App() {return (<Accordion defaultOpen={0}><Accordion.Item index={0}><Accordion.Header><Icon /> Custom Question with Icon</Accordion.Header><Accordion.Content><p>Answer with any content</p><button>Learn More</button></Accordion.Content></Accordion.Item><Accordion.Item index={1}><Accordion.Header>Another Question</Accordion.Header><Accordion.Content><CustomComponent /></Accordion.Content></Accordion.Item></Accordion>);}export default App;// Benefits:// ✅ Full control over structure// ✅ Easy to customize each item// ✅ State shared automatically via Context
🪝 Hooks
Stop copy-pasting the same useState + useEffect combo everywhere! 🛑 Custom Hooks let you extract reusable logic into clean, testable functions. Write it once, use it everywhere — your future self will thank you! 🙏
🔴 Impact: CRITICAL — The #1 pattern every React developer must master. If you're duplicating logic across components, you're doing it wrong! 🚨
📋 In this section: Extracting reusable logic • useUser custom hook • Eliminating code duplication • Shared state patterns
Extrae lógica cuando la repites en varios componentes (datos, auth, formularios). Un solo hook = una sola fuente de verdad y tests más simples.
// ❌ WRONG: Logic duplicated in multiple components// Mock API for demo (in real app this would be fetch)const mockFetchUser = (id) => Promise.resolve({ name: 'Jane Doe', avatar: 'https://via.placeholder.com/48' });function UserProfile({ userId }) {const [user, setUser] = useState(null);const [loading, setLoading] = useState(true);const [error, setError] = useState(null);useEffect(() => {setLoading(true);mockFetchUser(userId).then(data => { setUser(data); setLoading(false); }).catch(err => { setError(err.message); setLoading(false); });}, [userId]);if (loading) return <div>Loading...</div>;if (error) return <div>Error: {error}</div>;return <div>{user.name}</div>;}function UserAvatar({ userId }) {const [user, setUser] = useState(null);const [loading, setLoading] = useState(true);useEffect(() => {mockFetchUser(userId).then(setUser).finally(() => setLoading(false));}, [userId]);if (loading) return <div style={{ width: 48, height: 48, background: '#333', borderRadius: 24 }} />;return <span>{user?.name} (avatar)</span>;}function App() {return (<div><UserProfile userId="1" /><UserAvatar userId="1" /></div>);}export default App;// Problems: Code duplication, hard to maintain, logic not reusable
// ✅ GOOD: Extract logic into custom hook// Mock API for democonst mockFetchUser = (id) => Promise.resolve({ name: 'Jane Doe', avatar: 'https://via.placeholder.com/48' });function Skeleton() {return <div style={{ width: 48, height: 48, background: '#333', borderRadius: 24 }} />;}function useUser(userId) {const [user, setUser] = useState(null);const [loading, setLoading] = useState(true);const [error, setError] = useState(null);useEffect(() => {setLoading(true);setError(null);mockFetchUser(userId).then(data => { setUser(data); setLoading(false); }).catch(err => { setError(err.message); setLoading(false); });}, [userId]);return { user, loading, error };}function UserProfile({ userId }) {const { user, loading, error } = useUser(userId);if (loading) return <div>Loading...</div>;if (error) return <div>Error: {error}</div>;return <div>{user.name}</div>;}function UserAvatar({ userId }) {const { user, loading } = useUser(userId);if (loading) return <Skeleton />;return <span>{user?.name} (avatar)</span>;}function App() {return (<div><UserProfile userId="1" /><UserAvatar userId="1" /></div>);}export default App;
🎁 HOC
Wrap your components in superpowers! 🦸 HOCs are like gift wrapping — they take a component and return an enhanced version with extra abilities (auth, logging, theming). Think of them as component decorators! ✨
🔵 Impact: MEDIUM — A classic pattern that's still relevant for cross-cutting concerns like auth guards and analytics wrappers! 🔐
📋 In this section: Authentication HOC • Component enhancement • Cross-cutting concerns • withAuth pattern
Úsalos para añadir capacidades transversales (autenticación, permisos, logging) sin duplicar lógica en cada pantalla.
// ❌ WRONG: Authentication logic duplicatedconst checkAuth = () => Promise.resolve({ name: 'Demo User' });const redirect = () => {};function Dashboard() {const [user, setUser] = useState(null);const [loading, setLoading] = useState(true);useEffect(() => {checkAuth().then(u => {if (!u) { redirect('/login'); return; }setUser(u);setLoading(false);});}, []);if (loading) return <div>Loading...</div>;return <div>Dashboard Content</div>;}function Profile() {const [user, setUser] = useState(null);const [loading, setLoading] = useState(true);useEffect(() => {checkAuth().then(u => {if (!u) { redirect('/login'); return; }setUser(u);setLoading(false);});}, []);if (loading) return <div>Loading...</div>;return <div>Profile</div>;}function App() {return (<div><Dashboard /><Profile /></div>);}export default App;
// ✅ GOOD: Use HOC to add authenticationconst checkAuth = () => Promise.resolve({ name: 'Demo User' });const useRouter = () => ({ push: () => {} });function withAuth(Component) {return function AuthenticatedComponent(props) {const [user, setUser] = useState(null);const [loading, setLoading] = useState(true);const router = useRouter();useEffect(() => {checkAuth().then(u => {if (!u) { router.push('/login'); return; }setUser(u);setLoading(false);}).catch(() => router.push('/login'));}, []);if (loading) return <div>Loading...</div>;if (!user) return null;return <Component {...props} user={user} />;};}function Dashboard({ user }) {return <div>Dashboard: {user?.name}</div>;}function Profile({ user }) {return <div>Profile: {user?.name}</div>;}function Settings({ user }) {return <div>Settings: {user?.name}</div>;}const ProtectedDashboard = withAuth(Dashboard);const ProtectedProfile = withAuth(Profile);const ProtectedSettings = withAuth(Settings);function App() {return (<div><ProtectedDashboard /><ProtectedProfile /><ProtectedSettings /></div>);}export default App;
🎨 Render Props
Pass a function as children and let the consumer decide what to render! 🖌️ Render Props give you maximum flexibility — the component handles the logic, and YOU choose the presentation. It's like a choose-your-own-adventure for UI! 📖
🔵 Impact: MEDIUM — The OG flexibility pattern! Still powerful for sharing behavior between components without coupling them together 🔗
📋 In this section: Function-as-children pattern • MouseTracker example • Decoupled rendering • Reusable behavior sharing
// ❌ WRONG: Tightly coupled, not reusablefunction MouseTracker() {const [position, setPosition] = useState({ x: 0, y: 0 });useEffect(() => {const handleMouseMove = (e) => setPosition({ x: e.clientX, y: e.clientY });window.addEventListener('mousemove', handleMouseMove);return () => window.removeEventListener('mousemove', handleMouseMove);}, []);return (<div><p>Mouse at: {position.x}, {position.y}</p></div>);}function App() {return <MouseTracker />;}export default App;
// ✅ GOOD: Render prop pattern - flexible and reusablefunction MouseTracker({ children }) {const [position, setPosition] = useState({ x: 0, y: 0 });useEffect(() => {const handleMouseMove = (e) => setPosition({ x: e.clientX, y: e.clientY });window.addEventListener('mousemove', handleMouseMove);return () => window.removeEventListener('mousemove', handleMouseMove);}, []);return children(position);}function App() {return (<div><MouseTracker>{({ x, y }) => <p>Mouse at: {x}, {y}</p>}</MouseTracker></div>);}export default App;
💧 Hidratación
React 18 changed the game with Selective Hydration! 🎮 Instead of hydrating your entire app at once (blocking everything), React can now hydrate components in priority order. Users click a button? That component gets hydrated FIRST! It's like a VIP line for interactivity! 🎪
🟠 Impact: HIGH — This is how Netflix and Meta keep their apps feeling instant. Streaming SSR + selective hydration = blazing fast perceived performance! 🚀
📋 In this section: Traditional SSR waterfall • Streaming HTML • Suspense boundaries • Priority-based hydration
Envuelve componentes pesados o que dependen de datos en Suspense para que el resto de la página sea interactivo antes.
Visualizing Selective Hydration Priority
// ❌ TRADITIONAL SSR: Blocking "Waterfall"// server.jsimport { renderToString } from 'react-dom/server';// 1. Wait for ALL data to be ready// 2. Render ENTIRE tree to HTMLconst html = renderToString(<App />);// 3. Send HTML to clientres.send(html);// 4. Client waits for ALL JS to load// 5. React hydrates ENTIRE tree at once// 🛑 Result: Page is non-interactive until everything loads
// ✅ SELECTIVE HYDRATION (React 18+)// server.jsimport { pipeToNodeWritable } from 'react-dom/server';// 1. Start streaming HTML immediatelyconst { startWriting } = pipeToNodeWritable(<DataProvider data={data}><App /></DataProvider>,res,{onReadyToStream() {res.setHeader('Content-type', 'text/html');res.write('<!DOCTYPE html>');startWriting();}});// App.js// 2. Wrap slow components in Suspense<Suspense fallback={<Spinner />}><Comments /> {/* Large data fetching component */}</Suspense><Suspense fallback={<Spinner />}><Sidebar /></Suspense>// 🎯 Result:// - HTML streams in chunks// - User sees content sooner (First Contentful Paint)// - React hydrates interactive parts (like Sidebar) FIRST// - "Heavy" parts (Comments) hydrate later without blocking