Создание клиентских веб-приложений (SPA) в 2026 году избавилось от устаревших монструозных конфигураций Create React App и Webpack. Индустриальный стандарт построен на связке Vite 6, новой версии Tailwind CSS v4 с декларативными CSS-токенами, архитектурной методологии Feature-Sliced Design и библиотеки TanStack Query v5. Такой стек гарантирует идеальный Developer Experience и масштабируемость кодовой базы на годы вперед.
Начиная разработку React-приложения хаотично («все компоненты в папке components»), команды гарантированно сталкиваются со следующими проблемами:
useEffect + useState приводит к race conditions (гонкам запросов), утечкам памяти при размонтировании, дублированию сетевых вызовов и отсутствию кэширования между переходами по страницам.Архитектура Feature-Sliced Design (FSD) организует код в виде строгой направленной иерархии слоев. Модуль на верхнем слое может импортировать модули только с нижележащих слоев, но никогда наоборот:
┌─────────────────────────────────────────────────────────┐
│ Layer 1: app/ (Провайдеры, стили, роутер, точка входа)
└───────────┬─────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ Layer 2: pages/ (Композиция страниц для маршрутов)
└───────────┬─────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ Layer 3: widgets/ (Самодостаточные блоки: Header, Sidebar)
└───────────┬─────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ Layer 4: features/ (Пользовательские сценарии: AuthByEmail)
└───────────┬─────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ Layer 5: entities/ (Бизнес-сущности: UserCard, ProductRow)
└───────────┬─────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ Layer 6: shared/ (UI-кит, утилиты, HTTP-клиент, типы)
└─────────────────────────────────────────────────────────┘Создание чистого шаблона с React 19 и TypeScript через pnpm create vite с настройкой path aliases @/*.
Подключение современного @tailwindcss/vite плагина и настройка переменных цветовой палитры прямо в основном CSS-файле без js-конфигов.
Создание слоев app, pages, widgets, features, entities и shared с изоляцией публичных API через index.ts.
Инициализация QueryClientProvider с оптимальными интервалами staleTime и сборщиком мусора gcTime.
Подключение глобальных провайдеров, Suspense-границ и рендеринг страницы с кастомными Query-хуками.
Почему хранение серверных данных (список пользователей, заказов) в обычном useState() считается устаревшим подходом по сравнению с TanStack Query v5?
Сервер разработки отдает исходные TSX файлы напрямую в браузер через нативные ES-модули без предварительного бандлинга.
Время холодного запуска проекта составляет менее 150 мс вне зависимости от размера кодовой базы.
Высокопроизводительный движок сканирует разметку компонентов и компилирует только используемые утилитарные классы.
CSS-first архитектура с токенами @theme ускоряет компиляцию стилей в 10 раз по сравнению с v3.
Сетевые запросы дедуплицируются: если два компонента на экране запрашивают один и тот же ключ queryKey: ['users'], запрос уходит один раз.
Автоматический повтор (retry: 2) при сетевых сбоях и поддержка фонового обновления.
React 19 монтирует интерфейс с поддержкой плавных Suspense переходов, Action transitions и минимальным количеством перерисовок.
Полноценная изоляция страниц и виджетов по правилам Feature-Sliced Design.
Ниже приведены эталонные файлы архитектуры SPA.
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
import path from 'node:path';
// https://vite.dev/config/
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
port: 3000,
open: true,
},
build: {
target: 'esnext',
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
'react-vendor': ['react', 'react-dom'],
'query-vendor': ['@tanstack/react-query'],
},
},
},
},
});@import "tailwindcss";
@theme {
--color-primary: #8b5cf6;
--color-primary-hover: #7c3aed;
--color-bg-surface: #0f172a;
--color-bg-card: #1e293b;
--color-border-subtle: #334155;
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
--radius-card: 1rem;
}
body {
background-color: var(--color-bg-surface);
color: #f8fafc;
font-family: var(--font-sans);
margin: 0;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}import React, { useState } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
interface QueryProviderProps {
children: React.ReactNode;
}
export const QueryProvider: React.FC<QueryProviderProps> = ({ children }) => {
// Используем useState для создания экземпляра один раз на жизненный цикл приложения
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // Данные считаются свежими 5 минут (нет лишних refetch)
gcTime: 1000 * 60 * 30, // Хранить неиспользуемый кэш в памяти 30 минут
retry: 2, // 2 автоматических повтора при сбое сети
refetchOnWindowFocus: true, // Фоновое обновление при возврате на вкладку
},
},
})
);
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
};import { useQuery } from '@tanstack/react-query';
export interface User {
readonly id: string;
readonly name: string;
readonly email: string;
readonly role: 'Admin' | 'Developer' | 'Viewer';
}
async function fetchUsersList(): Promise<User[]> {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) {
throw new Error(`Failed to load users: ${response.statusText}`);
}
const raw = await response.json();
return raw.map((u: any) => ({
id: String(u.id),
name: u.name,
email: u.email,
role: u.id % 2 === 0 ? 'Developer' : 'Admin',
}));
}
export function useUsers() {
return useQuery({
queryKey: ['entities', 'users', 'list'],
queryFn: fetchUsersList,
});
}import React from 'react';
import { createRoot } from 'react-dom/client';
import { QueryProvider } from '@/app/providers/query-provider';
import { UsersPage } from '@/pages/users';
import '@/app/styles/index.css';
const container = document.getElementById('root');
if (!container) {
throw new Error('Root container element was not found in index.html');
}
const root = createRoot(container);
root.render(
<React.StrictMode>
<QueryProvider>
<UsersPage />
</QueryProvider>
</React.StrictMode>
);// src/pages/users/ui/UsersPage.tsx
import React from 'react';
import { useUsers } from '@/entities/user/api/use-users';
export const UsersPage: React.FC = () => {
const { data: users, isLoading, isError, error, refetch } = useUsers();
if (isLoading) {
return (
<div className="flex h-64 items-center justify-center">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-violet-500 border-t-transparent" />
</div>
);
}
if (isError) {
return (
<div className="rounded-card border border-red-500/20 bg-red-950/20 p-6 text-center">
<p className="text-red-400 font-medium">Произошла ошибка загрузки данных: {error?.message}</p>
<button
onClick={() => refetch()}
className="mt-4 rounded-lg bg-violet-600 px-4 py-2 text-white hover:bg-violet-700 transition"
>
Повторить попытку
</button>
</div>
);
}
return (
<main className="mx-auto max-w-4xl px-4 py-12">
<header className="mb-8 flex items-center justify-between">
<h1 className="text-3xl font-bold tracking-tight text-white">Команда разработки</h1>
<span className="rounded-full bg-violet-950/60 border border-violet-500/30 px-3 py-1 text-sm text-violet-300">
Всего: {users?.length ?? 0}
</span>
</header>
<div className="grid gap-4 sm:grid-cols-2">
{users?.map((user) => (
<div
key={user.id}
className="rounded-card border border-border-subtle bg-bg-card p-5 transition hover:border-violet-500/50"
>
<div className="flex items-center justify-between">
<h2 className="font-semibold text-lg text-slate-100">{user.name}</h2>
<span className="text-xs font-mono uppercase text-violet-400">{user.role}</span>
</div>
<p className="mt-1 text-sm text-slate-400">{user.email}</p>
</div>
))}
</div>
</main>
);
};Если написать const client = new QueryClient(); внутри функционального компонента без useState(() => new QueryClient()), то при каждом ререндере родительского компонента будет создаваться совершенно новый экземпляр кэша. В результате весь сохраненный кэш запросов сотрется, и приложение уйдет в бесконечный цикл повторной загрузки данных.
В Tailwind CSS v4 файл конфигурации JavaScript устарел. Все кастомные цвета, шрифты, радиусы и брейкпоинты теперь объявляются напрямую в CSS с помощью директивы @theme. Создание tailwind.config.js либо игнорируется новым компилятором Oxide, либо приводит к конфликтам сборки.
Попытка импортировать компонент или хук из вышележащего слоя (например, импорт Widget внутри Entity) грубо нарушает принципы FSD. Это порождает круговые зависимости (Circular Dependencies) и делает изолированное тестирование бизнес-сущностей невозможным. Зависимости обязаны течь строго сверху вниз.
Если ваш запрос зависит от параметра (например, userId), но в вызове указан статический ключ queryKey: ['user'], TanStack Query вернет закэшированные данные первого запрошенного пользователя для всех последующих. Всегда включайте динамические параметры в ключ: queryKey: ['user', userId].
В приложении на двух разных экранах вызывается хук useUsers() с одинаковым ключом queryKey: ['entities', 'users', 'list']. Пользователь быстро переключается между экранами. Сколько HTTP-запросов отправит браузер при установленном staleTime: 5 минут?