Кладовочка

О разном, чтобы было под рукой онлайн

React: шпаргалка — хуки, React 19, Next.js, Zustand, TypeScript, формы

React — самая популярная библиотека для создания пользовательских интерфейсов. Шпаргалка покрывает все хуки, React 19, Next.js, стейт-менеджмент и решения реальных проблем.

Компоненты и JSX

// Функциональный компонент
function Greeting({ name }) { return <h1>Привет, {name}!</h1> }
const Greeting = ({ name }) => <h1>Привет, {name}!</h1>

// Выражения, условия, списки
const el = <h1>Привет, {name}!</h1>
{isLoggedIn && <Dashboard />}
{isLoggedIn ? <Dashboard /> : <Login />}
{users.map(u => <UserCard key={u.id} user={u} />)}  // key = стабильный ID!

// Стили
<div className="card active" style={{ marginTop: 8 }} />

Хуки: полный справочник

useState — состояние

const [count, setCount] = useState(0)
const [user, setUser] = useState({ name: '', age: 0 })

setCount(42)                          // новое значение
setCount(prev => prev + 1)            // функциональная форма (надёжнее!)
setUser({ ...user, name: 'Иван' })    // ❌ user.name = 'Иван' — мутация не сработает
setItems([...items, newItem])         // ❌ items.push() — мутация не сработает

const [data, setData] = useState(() => expensiveComputation())  // lazy init

useEffect — эффекты

// Монтирование
useEffect(() => { fetchData().then(setData) }, [])

// Слежение за зависимостью
useEffect(() => { fetchUser(id).then(setUser) }, [id])

// Очистка (таймеры, подписки, fetch-отмена)
useEffect(() => {
  const controller = new AbortController()
  fetch(url, { signal: controller.signal }).then(...)
  const timer = setInterval(tick, 1000)
  return () => { controller.abort(); clearInterval(timer) }
}, [url])

useRef — ссылка без ререндера

const inputRef = useRef(null)
<input ref={inputRef} />
inputRef.current.focus()

// Хранение предыдущего значения
const prevValue = useRef(value)
useEffect(() => { prevValue.current = value }, [value])

// Счётчик без ререндера (НЕ для UI!)
countRef.current += 1

useContext — глобальное состояние

const ThemeContext = createContext('light')
function App() {
  return <ThemeContext.Provider value="dark"><Page /></ThemeContext.Provider>
}
function Button() {
  const theme = useContext(ThemeContext)
  return <button className={theme}>Кнопка</button>
}

useMemo / useCallback — мемоизация

const sorted = useMemo(() => items.sort((a, b) => a.price - b.price), [items])
const handleClick = useCallback((id) => { setSelected(id) }, [])

// ❌ Не мемоизировать без профилирования! «На всякий случай» — вредно

useReducer — сложная логика

const reducer = (state, action) => {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 }
    case 'reset':     return { count: 0 }
    default:          return state
  }
}
const [state, dispatch] = useReducer(reducer, { count: 0 })

Кастомные хуки

function useFetch(url) {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)
  useEffect(() => {
    const ctrl = new AbortController()
    setLoading(true)
    fetch(url, { signal: ctrl.signal }).then(r => r.json()).then(setData)
      .catch(e => e.name !== 'AbortError' && setError(e))
      .finally(() => setLoading(false))
    return () => ctrl.abort()
  }, [url])
  return { data, loading }
}

function useDebounce(value, delay = 300) {
  const [debounced, setDebounced] = useState(value)
  useEffect(() => { const t = setTimeout(() => setDebounced(value), delay); return () => clearTimeout(t) }, [value, delay])
  return debounced
}

React 19: новые хуки

// useOptimistic — мгновенное обновление UI с откатом при ошибке
function TodoList() {
  const [todos, setTodos] = useState([])
  const [optimisticTodos, addOptimistic] = useOptimistic(todos,
    (state, newTodo) => [...state, { text: newTodo, pending: true }]
  )
  async function addTodo(formData) {
    addOptimistic(formData.get('text'))
    await saveToServer(formData)
    setTodos(prev => [...prev, { text: formData.get('text') }])
  }
}

// useActionState — серверный экшен с состоянием (замена useFormState)
function ChangeName() {
  const [state, submitAction, isPending] = useActionState(updateName, { error: null })
  return (
    <form action={submitAction}>
      <input name="name" />
      <button disabled={isPending}>Сохранить</button>
      {state?.error && <p className="error">{state.error}</p>}
    </form>
  )
}

// useFormStatus — статус родительской формы
function SubmitButton() {
  const { pending, data, method } = useFormStatus()
  return <button disabled={pending}>{pending ? 'Сохранение...' : 'Сохранить'}</button>
}

React 19: Server Components (RSC)

// Серверный компонент (по умолчанию в Next.js App Router)
async function UserList() {
  const users = await db.query('SELECT * FROM users')  // прямой доступ к БД
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}

// Клиентский компонент
'use client'
function LikeButton() {
  const [liked, setLiked] = useState(false)
  return <button onClick={() => setLiked(!liked)}>{liked ? '❤️' : '🤍'}</button>
}

Next.js App Router

// app/layout.tsx — корневой layout
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return <html><body><Header />{children}<Footer /></body></html>
}

// app/users/page.tsx — страница с загрузкой данных
export default async function UsersPage() {
  const users = await fetch('https://api.example.com/users').then(r => r.json())
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}

// app/users/[id]/page.tsx — динамическая страница
export default async function UserPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  const user = await fetch(`https://api.example.com/users/${id}`).then(r => r.json())
  return <h1>{user.name}</h1>
}

// Навигация
import Link from 'next/link'
import { useRouter } from 'next/navigation'
<Link href="/users">Пользователи</Link>
const router = useRouter(); router.push('/users')

Zustand — лёгкий стейт-менеджмент

import { create } from 'zustand'

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  reset: () => set({ count: 0 }),
  fetchData: async (id) => {
    const data = await api.fetch(id)
    set({ data })
  },
}))

function Counter() {
  const count = useStore((s) => s.count)        // селектор — только на count!
  const increment = useStore((s) => s.increment)
  return <button onClick={increment}>{count}</button>
}

React Hook Form — формы без боли

import { useForm } from 'react-hook-form'

function SignupForm() {
  const { register, handleSubmit, formState: { errors } } = useForm()

  return (
    <form onSubmit={handleSubmit(data => console.log(data))}>
      <input {...register('email', { required: 'Email обязателен', pattern: { value: /^\S+@\S+$/i, message: 'Неверный email' } })} />
      {errors.email && <span>{errors.email.message}</span>}
      <input {...register('password', { required: true, minLength: 8 })} />
      <button type="submit">Отправить</button>
    </form>
  )
}

TypeScript с React

// Пропсы
interface Props {
  name: string
  age?: number
  children: React.ReactNode
  onClick?: () => void
}
function UserCard({ name, age, children, onClick }: Props) { return <div onClick={onClick}>{name} — {age}{children}</div> }

// useState с типами
const [count, setCount] = useState<number>(0)
const [user, setUser] = useState<User | null>(null)
const [status, setStatus] = useState<'idle' | 'loading' | 'error'>('idle')

// useRef: readonly vs mutable
const inputRef = useRef<HTMLInputElement>(null)  // readonly
const timerRef = useRef<number | null>(null)      // mutable (нет readonly)

// Event handlers
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => setName(e.target.value)
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault() }

// Children types
type Props = { children: React.ReactNode }          // любой JSX
type CardProps = { renderHeader: () => React.ReactNode }  // render prop

React Router v7

import { BrowserRouter, Routes, Route, Link, useParams, useNavigate, Outlet } from 'react-router-dom'

// Layout с <Outlet>
function DashboardLayout() {
  return <div><Sidebar /><Outlet /></div>
}

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="dashboard" element={<DashboardLayout />}>
    <Route index element={<Overview />} />
    <Route path="users/:id" element={<UserDetail />} />
  </Route>
</Routes>

Производительность

const Expensive = React.memo(({ data }: { data: Item[] }) => <HeavyRendering data={data} />)

const LazyPage = React.lazy(() => import('./HeavyPage'))
<Suspense fallback={<Spinner />}><LazyPage /></Suspense>

const [isPending, startTransition] = useTransition()
startTransition(() => setSearchQuery(value))

Правила хуков

  1. Вызывать только на верхнем уровне — не внутри условий, циклов
  2. Только из React-компонентов или кастомных хуков
  3. Кастомные хуки начинать с use
// ❌ Нарушение
if (condition) { useEffect(() => {}) }
// ✅ Правильно
useEffect(() => { if (condition) {} }, [condition])

Частые проблемы и их решения

Проблема ❌ Ошибка ✅ Решение
Бесконечный useEffect useEffect(() => { setCount(count + 1) }, [count]) useEffect(() => { setCount(prev => prev + 1) }, [])
Замыкание старого состояния setTimeout(() => console.log(count), 3000) const ref = useRef(count); ref.current = count
Мутация состояния items.push(x); setItems(items) setItems([...items, x])
Контекст ререндерит всё дерево Один контекст на всё приложение Разделить на мелкие контексты
Утечка без cleanup useEffect(() => { subscribe() }) useEffect(() => { const u = subscribe(); return u }, [])
useCallback без смысла Каждая функция в useCallback Только при передаче в React.memo
key по индексу items.map((_, i) => <li key={i}> items.map(i => <li key={i.id}>
setState после unmount Асинхронный setState без проверки useEffect(() => { let ok = true; fetch().then(d => ok && setData(d)); return () => { ok = false } }, [])

Полезные ресурсы


Что дальше

Связанные шпаргалки: Vue 3, Svelte 5, TypeScript, Next.js, Docker