Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds
Volver a colecciones
Guide13 min read

React

React is the most popular JavaScript library for building user interfaces. Learn components, hooks, state management, and best practices in 2025.

React - Complete guide to the library that changed frontend development

What is React and why did it dominate the market?

React is a JavaScript library for building user interfaces, created by Facebook (Meta) in 2013. Over the course of a decade, it has become the de facto standard in the world of frontend development, used by Netflix, Airbnb, Instagram, WhatsApp, and millions of other applications.

React introduced revolutionary concepts:

  • Component-based thinking - UI as a tree of reusable components
  • Virtual DOM - Efficient interface updates
  • Unidirectional data flow - Predictable state management
  • JSX - HTML in JavaScript

Why React in 2025?

Massive ecosystem

React has the largest ecosystem in the frontend world:

  • Next.js, Remix - Full-stack frameworks
  • React Native - Mobile applications
  • Thousands of libraries - A solution for every problem

Stability and backward compatibility

Meta invests enormous resources in maintaining React. Breaking changes are rare and well communicated.

Demand on the job market

React is the most frequently required skill for frontend developers. Knowledge of React opens the door to thousands of job offers.

React fundamentals

First component

Code
TypeScript
// Functional component - the standard in 2025
function Welcome({ name }: { name: string }) {
  return <h1>Hello, {name}!</h1>
}

// Usage
function App() {
  return (
    <div>
      <Welcome name="Anna" />
      <Welcome name="Bartek" />
    </div>
  )
}

JSX - Syntax

Code
TypeScript
function ProductCard({ product }: { product: Product }) {
  return (
    // JSX lets you write HTML-like syntax in JavaScript
    <div className="card">
      {/* JavaScript expressions in curly braces */}
      <h2>{product.name}</h2>
      <p>{product.description}</p>
      <span>${product.price.toFixed(2)}</span>

      {/* Conditional rendering */}
      {product.inStock ? (
        <button>Buy now</button>
      ) : (
        <span>Unavailable</span>
      )}

      {/* Mapping lists */}
      <ul>
        {product.features.map((feature, index) => (
          <li key={index}>{feature}</li>
        ))}
      </ul>
    </div>
  )
}

Props - Passing data

Code
TypeScript
// Defining prop types
interface ButtonProps {
  children: React.ReactNode
  variant?: 'primary' | 'secondary' | 'danger'
  size?: 'sm' | 'md' | 'lg'
  disabled?: boolean
  onClick?: () => void
}

// Component with props
function Button({
  children,
  variant = 'primary',
  size = 'md',
  disabled = false,
  onClick
}: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  )
}

// Usage
<Button variant="primary" size="lg" onClick={() => alert('Clicked!')}>
  Click me
</Button>

React Hooks - The heart of modern React

useState - Local state

Code
TypeScript
import { useState } from 'react'

function Counter() {
  // [value, setter function] = useState(initial value)
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>Counter: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount(count - 1)}>-1</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  )
}

// State with an object
function UserForm() {
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0
  })

  const updateField = (field: string, value: string | number) => {
    setUser(prev => ({
      ...prev,
      [field]: value
    }))
  }

  return (
    <form>
      <input
        value={user.name}
        onChange={(e) => updateField('name', e.target.value)}
      />
      <input
        value={user.email}
        onChange={(e) => updateField('email', e.target.value)}
      />
    </form>
  )
}

useEffect - Side effects

Code
TypeScript
import { useState, useEffect } from 'react'

function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)

  // Run the effect when userId changes
  useEffect(() => {
    const fetchUser = async () => {
      setLoading(true)
      setError(null)

      try {
        const response = await fetch(`/api/users/${userId}`)
        if (!response.ok) throw new Error('User not found')
        const data = await response.json()
        setUser(data)
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Error')
      } finally {
        setLoading(false)
      }
    }

    fetchUser()
  }, [userId]) // Dependency array

  if (loading) return <p>Loading...</p>
  if (error) return <p>Error: {error}</p>
  if (!user) return <p>No user found</p>

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  )
}

// useEffect with cleanup
function WindowSize() {
  const [size, setSize] = useState({ width: 0, height: 0 })

  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      })
    }

    // Set initial size
    handleResize()

    // Add listener
    window.addEventListener('resize', handleResize)

    // Cleanup - remove listener on unmount
    return () => {
      window.removeEventListener('resize', handleResize)
    }
  }, []) // Empty array = run once on mount

  return <p>Window size: {size.width} x {size.height}</p>
}

useContext - Global state

Code
TypeScript
import { createContext, useContext, useState, ReactNode } from 'react'

// 1. Create context
interface ThemeContextType {
  theme: 'light' | 'dark'
  toggleTheme: () => void
}

const ThemeContext = createContext<ThemeContextType | null>(null)

// 2. Provider component
function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light')

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light')
  }

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

// 3. Custom hook for easier usage
function useTheme() {
  const context = useContext(ThemeContext)
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider')
  }
  return context
}

// 4. Usage in components
function ThemeToggle() {
  const { theme, toggleTheme } = useTheme()

  return (
    <button onClick={toggleTheme}>
      Current theme: {theme}
    </button>
  )
}

function App() {
  return (
    <ThemeProvider>
      <ThemeToggle />
      <MainContent />
    </ThemeProvider>
  )
}

useRef - References and non-mutating values

Code
TypeScript
import { useRef, useEffect } from 'react'

// Reference to a DOM element
function AutoFocusInput() {
  const inputRef = useRef<HTMLInputElement>(null)

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

  return <input ref={inputRef} placeholder="Auto-focus" />
}

// Storing values without re-rendering
function Timer() {
  const [count, setCount] = useState(0)
  const intervalRef = useRef<number | null>(null)

  const start = () => {
    if (intervalRef.current) return
    intervalRef.current = window.setInterval(() => {
      setCount(c => c + 1)
    }, 1000)
  }

  const stop = () => {
    if (intervalRef.current) {
      clearInterval(intervalRef.current)
      intervalRef.current = null
    }
  }

  return (
    <div>
      <p>Time: {count}s</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  )
}

useMemo and useCallback - Optimization

Code
TypeScript
import { useMemo, useCallback, useState } from 'react'

function ExpensiveList({ items, filter }: { items: Item[]; filter: string }) {
  // useMemo - caches the result of calculations
  const filteredItems = useMemo(() => {
    console.log('Filtering...')
    return items.filter(item =>
      item.name.toLowerCase().includes(filter.toLowerCase())
    )
  }, [items, filter]) // Recalculate only when items or filter changes

  return (
    <ul>
      {filteredItems.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  )
}

function Parent() {
  const [count, setCount] = useState(0)

  // useCallback - caches the function
  const handleClick = useCallback(() => {
    console.log('Clicked!')
  }, []) // Function does not change

  // Without useCallback - Child would re-render on every count change
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      <Child onClick={handleClick} />
    </div>
  )
}

useReducer - Complex state

Code
TypeScript
import { useReducer } from 'react'

// Types
interface State {
  items: Item[]
  loading: boolean
  error: string | null
}

type Action =
  | { type: 'FETCH_START' }
  | { type: 'FETCH_SUCCESS'; payload: Item[] }
  | { type: 'FETCH_ERROR'; payload: string }
  | { type: 'ADD_ITEM'; payload: Item }
  | { type: 'REMOVE_ITEM'; payload: string }

// Reducer
function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true, error: null }
    case 'FETCH_SUCCESS':
      return { ...state, loading: false, items: action.payload }
    case 'FETCH_ERROR':
      return { ...state, loading: false, error: action.payload }
    case 'ADD_ITEM':
      return { ...state, items: [...state.items, action.payload] }
    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter(item => item.id !== action.payload)
      }
    default:
      return state
  }
}

// Component
function ItemList() {
  const [state, dispatch] = useReducer(reducer, {
    items: [],
    loading: false,
    error: null
  })

  const fetchItems = async () => {
    dispatch({ type: 'FETCH_START' })
    try {
      const response = await fetch('/api/items')
      const data = await response.json()
      dispatch({ type: 'FETCH_SUCCESS', payload: data })
    } catch (err) {
      dispatch({ type: 'FETCH_ERROR', payload: 'Fetch error' })
    }
  }

  return (
    <div>
      {state.loading && <p>Loading...</p>}
      {state.error && <p>Error: {state.error}</p>}
      <button onClick={fetchItems}>Fetch</button>
      <ul>
        {state.items.map(item => (
          <li key={item.id}>
            {item.name}
            <button onClick={() => dispatch({ type: 'REMOVE_ITEM', payload: item.id })}>
              Remove
            </button>
          </li>
        ))}
      </ul>
    </div>
  )
}

Custom Hooks - Building your own hooks

Code
TypeScript
// useFetch - universal hook for data fetching
function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    const controller = new AbortController()

    const fetchData = async () => {
      setLoading(true)
      setError(null)

      try {
        const response = await fetch(url, { signal: controller.signal })
        if (!response.ok) throw new Error('Network error')
        const json = await response.json()
        setData(json)
      } catch (err) {
        if (err instanceof Error && err.name !== 'AbortError') {
          setError(err.message)
        }
      } finally {
        setLoading(false)
      }
    }

    fetchData()

    return () => controller.abort()
  }, [url])

  return { data, loading, error }
}

// Usage
function UserList() {
  const { data: users, loading, error } = useFetch<User[]>('/api/users')

  if (loading) return <p>Loading...</p>
  if (error) return <p>Error: {error}</p>

  return (
    <ul>
      {users?.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  )
}

// useLocalStorage - synchronization with localStorage
function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    if (typeof window === 'undefined') return initialValue

    try {
      const item = window.localStorage.getItem(key)
      return item ? JSON.parse(item) : initialValue
    } catch {
      return initialValue
    }
  })

  const setValue = (value: T | ((val: T) => T)) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value
      setStoredValue(valueToStore)
      window.localStorage.setItem(key, JSON.stringify(valueToStore))
    } catch (error) {
      console.error('Error saving to localStorage', error)
    }
  }

  return [storedValue, setValue] as const
}

// useDebounce - delayed values
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value)

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value)
    }, delay)

    return () => clearTimeout(timer)
  }, [value, delay])

  return debouncedValue
}

// useDebounce usage
function SearchInput() {
  const [query, setQuery] = useState('')
  const debouncedQuery = useDebounce(query, 300)

  useEffect(() => {
    if (debouncedQuery) {
      // Search only after 300ms since the last change
      searchAPI(debouncedQuery)
    }
  }, [debouncedQuery])

  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="Search..."
    />
  )
}

Design patterns

Compound Components

Code
TypeScript
// Menu as a compound component
interface MenuProps {
  children: React.ReactNode
}

interface MenuItemProps {
  children: React.ReactNode
  onClick?: () => void
}

function Menu({ children }: MenuProps) {
  return <ul className="menu">{children}</ul>
}

function MenuItem({ children, onClick }: MenuItemProps) {
  return (
    <li className="menu-item" onClick={onClick}>
      {children}
    </li>
  )
}

function MenuDivider() {
  return <li className="menu-divider" />
}

// Assign subcomponents
Menu.Item = MenuItem
Menu.Divider = MenuDivider

// Usage
function App() {
  return (
    <Menu>
      <Menu.Item onClick={() => console.log('Home')}>Home</Menu.Item>
      <Menu.Item onClick={() => console.log('About')}>About</Menu.Item>
      <Menu.Divider />
      <Menu.Item onClick={() => console.log('Logout')}>Logout</Menu.Item>
    </Menu>
  )
}

Render Props

Code
TypeScript
interface MousePosition {
  x: number
  y: number
}

interface MouseTrackerProps {
  render: (position: MousePosition) => React.ReactNode
}

function MouseTracker({ render }: MouseTrackerProps) {
  const [position, setPosition] = useState({ x: 0, y: 0 })

  useEffect(() => {
    const handleMouseMove = (e: MouseEvent) => {
      setPosition({ x: e.clientX, y: e.clientY })
    }

    window.addEventListener('mousemove', handleMouseMove)
    return () => window.removeEventListener('mousemove', handleMouseMove)
  }, [])

  return <>{render(position)}</>
}

// Usage
function App() {
  return (
    <MouseTracker
      render={({ x, y }) => (
        <p>Mouse position: {x}, {y}</p>
      )}
    />
  )
}

Higher-Order Components (HOC)

Code
TypeScript
// HOC for adding loading state
function withLoading<P extends object>(
  Component: React.ComponentType<P>
) {
  return function WithLoading({ isLoading, ...props }: P & { isLoading: boolean }) {
    if (isLoading) {
      return <div className="spinner">Loading...</div>
    }
    return <Component {...(props as P)} />
  }
}

// Usage
const UserListWithLoading = withLoading(UserList)

function App() {
  const [loading, setLoading] = useState(true)
  return <UserListWithLoading isLoading={loading} users={users} />
}

Forms

Controlled components

Code
TypeScript
function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  })
  const [errors, setErrors] = useState<Record<string, string>>({})

  const handleChange = (
    e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
  ) => {
    const { name, value } = e.target
    setFormData(prev => ({ ...prev, [name]: value }))

    // Clear error on change
    if (errors[name]) {
      setErrors(prev => ({ ...prev, [name]: '' }))
    }
  }

  const validate = () => {
    const newErrors: Record<string, string> = {}

    if (!formData.name) newErrors.name = 'Name is required'
    if (!formData.email) newErrors.email = 'Email is required'
    else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      newErrors.email = 'Invalid email'
    }
    if (!formData.message) newErrors.message = 'Message is required'

    setErrors(newErrors)
    return Object.keys(newErrors).length === 0
  }

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()

    if (validate()) {
      console.log('Sending:', formData)
      // Send data
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">Name</label>
        <input
          id="name"
          name="name"
          value={formData.name}
          onChange={handleChange}
        />
        {errors.name && <span className="error">{errors.name}</span>}
      </div>

      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          name="email"
          type="email"
          value={formData.email}
          onChange={handleChange}
        />
        {errors.email && <span className="error">{errors.email}</span>}
      </div>

      <div>
        <label htmlFor="message">Message</label>
        <textarea
          id="message"
          name="message"
          value={formData.message}
          onChange={handleChange}
        />
        {errors.message && <span className="error">{errors.message}</span>}
      </div>

      <button type="submit">Send</button>
    </form>
  )
}

React 19+ Features

use() hook

Code
TypeScript
import { use, Suspense } from 'react'

// use() lets you read a Promise directly
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise) // Suspends the component

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  )
}

function App() {
  const userPromise = fetchUser(1)

  return (
    <Suspense fallback={<p>Loading...</p>}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  )
}

Actions (form actions)

Code
TypeScript
function SignupForm() {
  const [error, setError] = useState<string | null>(null)

  async function signup(formData: FormData) {
    const email = formData.get('email') as string
    const password = formData.get('password') as string

    try {
      await createAccount(email, password)
    } catch (err) {
      setError('Failed to create account')
    }
  }

  return (
    <form action={signup}>
      <input name="email" type="email" required />
      <input name="password" type="password" required />
      <button type="submit">Sign up</button>
      {error && <p className="error">{error}</p>}
    </form>
  )
}

Testing React

React Testing Library

Code
TypeScript
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

// Simple test
test('renders greeting', () => {
  render(<Welcome name="Anna" />)
  expect(screen.getByText('Hello, Anna!')).toBeInTheDocument()
})

// Interaction test
test('increments counter', async () => {
  render(<Counter />)

  const button = screen.getByRole('button', { name: /\+1/i })
  await userEvent.click(button)

  expect(screen.getByText(/Counter: 1/)).toBeInTheDocument()
})

// Async test
test('loads user data', async () => {
  render(<UserProfile userId="1" />)

  // Wait for loading to complete
  await waitFor(() => {
    expect(screen.getByText('Jan Kowalski')).toBeInTheDocument()
  })
})

// Mock API
import { rest } from 'msw'
import { setupServer } from 'msw/node'

const server = setupServer(
  rest.get('/api/users/1', (req, res, ctx) => {
    return res(ctx.json({ id: '1', name: 'Jan Kowalski' }))
  })
)

beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

Best practices

Do's

Code
TEXT
✅ Use functional components and hooks
✅ Keep components small and focused
✅ Extract logic into custom hooks
✅ Use TypeScript for type safety
✅ Memoize expensive calculations (useMemo)
✅ Use keys when mapping lists
✅ Handle loading and error states

Don'ts

Code
TEXT
❌ Don't mutate state directly
❌ Don't use index as key (unless the list is static)
❌ Don't overuse useEffect
❌ Don't keep all state in a single useState
❌ Don't ignore the dependency array in useEffect
❌ Don't create functions inside JSX without useCallback

React vs Alternatives

AspectReactVueSvelte
ApproachLibraryFrameworkCompiler
SyntaxJSXTemplatesTemplates
StateuseState, ReduxComposition APIStores
Learning curveMediumLowLow
EcosystemMassiveLargeGrowing
PerformanceGoodGoodExcellent

Summary

React is the foundation of modern frontend development:

  • Components - Reusable, modular code
  • Hooks - Elegant state and effect management
  • Ecosystem - A solution for every problem
  • Stability - Long-term support from Meta