Разработка фронтенда сайта на React

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка фронтенда сайта на React
Средняя
от 1 недели до 3 месяцев
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Разработка фронтенда сайта на React

React — библиотека для построения пользовательского интерфейса через декларативные компоненты. Сайт на React — это SPA или SSR-приложение с маршрутизацией, состоянием, типизацией, оптимизацией сборки и CI/CD. Здесь нет «Hello World» — только продакшн-архитектура для проекта, которому предстоит расти.

Стек и выбор фреймворка

Чистый Vite + React — для SPA без SEO-требований (панели управления, порталы):

React 18+ + Vite 5 + TypeScript + React Router v6 + Tanstack Query + Zustand

Next.js App Router — для сайтов с SEO, смешанным контентом, ISR:

Next.js 15 + TypeScript + React Server Components + Tanstack Query (client) + Zustand (client)

Remix — для форм, мутаций, fullstack без API-слоя:

Remix 2 + TypeScript + Prisma + Zod

Этот материал — о Vite + React SPA, Next.js — в отдельном разделе.

Инициализация и структура проекта

npm create vite@latest my-site -- --template react-ts
cd my-site
npm install

Расширить базовую установку:

# Маршрутизация
npm install react-router-dom

# Серверное состояние
npm install @tanstack/react-query @tanstack/react-query-devtools

# Глобальное состояние
npm install zustand

# Формы + валидация
npm install react-hook-form zod @hookform/resolvers

# UI
npm install @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-tooltip

# Утилиты
npm install clsx tailwind-merge class-variance-authority

# Стили
npm install -D tailwindcss @tailwindcss/vite @tailwindcss/typography

# Иконки
npm install lucide-react

# Dev
npm install -D @types/node eslint @typescript-eslint/eslint-plugin prettier

Структура директорий

src/
  app/                    ← Инициализация приложения
    App.tsx
    Router.tsx
    QueryProvider.tsx
  assets/                 ← Статика (изображения, шрифты)
  components/
    ui/                   ← Примитивы (Button, Input, Card, Dialog)
    layout/               ← Header, Footer, Sidebar, Section
    shared/               ← Переиспользуемые составные компоненты
  features/               ← Feature-слайсы (слабая связность)
    home/
      components/
      hooks/
      index.ts
    about/
    contact/
  hooks/                  ← Глобальные хуки
  lib/                    ← Утилиты, конфигурации
    utils.ts              ← cn() и прочее
    api.ts                ← Axios/Fetch конфигурация
    queryClient.ts
  pages/                  ← Страницы (используют features)
    HomePage.tsx
    AboutPage.tsx
    ContactPage.tsx
    NotFoundPage.tsx
  store/                  ← Zustand stores
  styles/
    globals.css
  types/                  ← Глобальные TypeScript типы

Маршрутизация

// src/app/Router.tsx
import { createBrowserRouter, RouterProvider, Outlet } from 'react-router-dom';
import { lazy, Suspense } from 'react';
import { AppShell } from '@/components/layout/AppShell';
import { PageLoader } from '@/components/ui/PageLoader';

// Ленивая загрузка страниц — каждая страница в отдельный chunk
const HomePage = lazy(() => import('@/pages/HomePage'));
const AboutPage = lazy(() => import('@/pages/AboutPage'));
const ServicesPage = lazy(() => import('@/pages/ServicesPage'));
const BlogPage = lazy(() => import('@/pages/BlogPage'));
const BlogPostPage = lazy(() => import('@/pages/BlogPostPage'));
const ContactPage = lazy(() => import('@/pages/ContactPage'));
const NotFoundPage = lazy(() => import('@/pages/NotFoundPage'));

const router = createBrowserRouter([
  {
    element: (
      <AppShell>
        <Suspense fallback={<PageLoader />}>
          <Outlet />
        </Suspense>
      </AppShell>
    ),
    children: [
      { path: '/', element: <HomePage /> },
      { path: '/about', element: <AboutPage /> },
      { path: '/services', element: <ServicesPage /> },
      {
        path: '/blog',
        children: [
          { index: true, element: <BlogPage /> },
          { path: ':slug', element: <BlogPostPage /> },
        ],
      },
      { path: '/contact', element: <ContactPage /> },
    ],
    errorElement: <NotFoundPage />,
  },
]);

export const Router = () => <RouterProvider router={router} />;

Серверное состояние: Tanstack Query

// src/lib/queryClient.ts
import { QueryClient } from '@tanstack/react-query';

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,         // 5 минут
      gcTime: 10 * 60 * 1000,           // 10 минут
      retry: (failureCount, error: any) => {
        if (error?.response?.status === 404) return false;
        return failureCount < 2;
      },
      refetchOnWindowFocus: false,
    },
  },
});
// src/features/blog/hooks/usePosts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/api';

export interface Post {
  id: number;
  slug: string;
  title: string;
  excerpt: string;
  category: string;
  publishedAt: string;
  coverImage: string;
}

const POSTS_QUERY_KEY = ['posts'] as const;

export const usePosts = (params?: { category?: string; page?: number }) => {
  return useQuery({
    queryKey: [...POSTS_QUERY_KEY, params],
    queryFn: () => api.get<Post[]>('/posts', { params }),
    placeholderData: (previousData) => previousData, // Предотвратить мигание при пагинации
  });
};

export const usePost = (slug: string) => {
  return useQuery({
    queryKey: [...POSTS_QUERY_KEY, slug],
    queryFn: () => api.get<Post>(`/posts/${slug}`),
    enabled: Boolean(slug),
  });
};

Компоненты UI: Button с CVA

// src/components/ui/Button.tsx
import { ButtonHTMLAttributes, FC, forwardRef } from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
import { Loader2 } from 'lucide-react';

const buttonVariants = cva(
  'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
  {
    variants: {
      variant: {
        default: 'bg-blue-600 text-white shadow hover:bg-blue-700',
        destructive: 'bg-red-600 text-white shadow hover:bg-red-700',
        outline: 'border border-gray-200 bg-white shadow-sm hover:bg-gray-50',
        secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
        ghost: 'hover:bg-gray-100 hover:text-gray-900',
        link: 'text-blue-600 underline-offset-4 hover:underline',
      },
      size: {
        default: 'h-9 px-4 py-2',
        sm: 'h-8 rounded-md px-3 text-xs',
        lg: 'h-10 rounded-md px-8',
        icon: 'h-9 w-9',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'default',
    },
  }
);

interface ButtonProps
  extends ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  loading?: boolean;
}

const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, loading, children, disabled, ...props }, ref) => {
    return (
      <button
        className={cn(buttonVariants({ variant, size }), className)}
        disabled={disabled || loading}
        ref={ref}
        {...props}
      >
        {loading && <Loader2 className="h-4 w-4 animate-spin" />}
        {children}
      </button>
    );
  }
);
Button.displayName = 'Button';

export { Button, buttonVariants };

Форма обратной связи

// src/features/contact/ContactForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useMutation } from '@tanstack/react-query';
import { Button } from '@/components/ui/Button';
import { api } from '@/lib/api';

const contactSchema = z.object({
  name: z.string().min(2, 'Минимум 2 символа').max(100),
  email: z.string().email('Некорректный email'),
  phone: z.string().optional(),
  subject: z.string().min(5, 'Минимум 5 символов'),
  message: z.string().min(20, 'Минимум 20 символов').max(2000),
  consent: z.literal(true, { errorMap: () => ({ message: 'Необходимо согласие' }) }),
});

type ContactFormData = z.infer<typeof contactSchema>;

export const ContactForm = () => {
  const {
    register,
    handleSubmit,
    reset,
    formState: { errors, isSubmitSuccessful },
  } = useForm<ContactFormData>({
    resolver: zodResolver(contactSchema),
  });

  const { mutate, isPending, isSuccess, isError } = useMutation({
    mutationFn: (data: ContactFormData) => api.post('/contact', data),
    onSuccess: () => reset(),
  });

  if (isSuccess) {
    return (
      <div className="rounded-lg bg-green-50 p-6 text-green-800">
        <p className="font-medium">Сообщение отправлено</p>
        <p className="mt-1 text-sm">Мы ответим в течение 24 часов.</p>
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit((data) => mutate(data))} className="space-y-5" noValidate>
      <div className="grid gap-5 sm:grid-cols-2">
        <div>
          <label className="mb-1.5 block text-sm font-medium text-gray-700" htmlFor="name">
            Имя <span className="text-red-500">*</span>
          </label>
          <input
            id="name"
            className={cn(
              'w-full rounded-md border px-3 py-2 text-sm shadow-sm',
              'focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500',
              errors.name ? 'border-red-500' : 'border-gray-300'
            )}
            placeholder="Иван Иванов"
            {...register('name')}
          />
          {errors.name && (
            <p className="mt-1 text-xs text-red-600">{errors.name.message}</p>
          )}
        </div>

        <div>
          <label className="mb-1.5 block text-sm font-medium text-gray-700" htmlFor="email">
            Email <span className="text-red-500">*</span>
          </label>
          <input
            id="email"
            type="email"
            className={cn(
              'w-full rounded-md border px-3 py-2 text-sm shadow-sm',
              'focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500',
              errors.email ? 'border-red-500' : 'border-gray-300'
            )}
            placeholder="[email protected]"
            {...register('email')}
          />
          {errors.email && (
            <p className="mt-1 text-xs text-red-600">{errors.email.message}</p>
          )}
        </div>
      </div>

      <div>
        <label className="mb-1.5 block text-sm font-medium text-gray-700" htmlFor="message">
          Сообщение <span className="text-red-500">*</span>
        </label>
        <textarea
          id="message"
          rows={5}
          className={cn(
            'w-full rounded-md border px-3 py-2 text-sm shadow-sm',
            'focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500',
            errors.message ? 'border-red-500' : 'border-gray-300'
          )}
          {...register('message')}
        />
        {errors.message && (
          <p className="mt-1 text-xs text-red-600">{errors.message.message}</p>
        )}
      </div>

      {isError && (
        <p className="text-sm text-red-600">Ошибка отправки. Попробуйте ещё раз.</p>
      )}

      <Button type="submit" loading={isPending} size="lg" className="w-full sm:w-auto">
        Отправить сообщение
      </Button>
    </form>
  );
};

Оптимизация производительности

Сборка Vite: code splitting и tree-shaking

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: { '@': path.resolve(__dirname, 'src') },
  },
  build: {
    target: 'es2020',
    rollupOptions: {
      output: {
        manualChunks: {
          // Разделить vendor на отдельные чанки
          'vendor-react': ['react', 'react-dom', 'react-router-dom'],
          'vendor-query': ['@tanstack/react-query'],
          'vendor-forms': ['react-hook-form', 'zod', '@hookform/resolvers'],
        },
      },
    },
    // Не предупреждать о чанках до 700 KB (после — оптимизировать)
    chunkSizeWarningLimit: 700,
  },
});

React.memo и useMemo

// Тяжёлый список — мемоизировать
const PostCard = React.memo<{ post: Post }>(({ post }) => {
  return (
    <article className="...">
      <h2>{post.title}</h2>
      <p>{post.excerpt}</p>
    </article>
  );
}, (prevProps, nextProps) => prevProps.post.id === nextProps.post.id);

// Тяжёлые вычисления
const sortedPosts = useMemo(
  () => [...posts].sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()),
  [posts]
);

Intersection Observer для lazy rendering

import { useIntersectionObserver } from '@/hooks/useIntersectionObserver';

const LazySection: FC<{ children: ReactNode }> = ({ children }) => {
  const { ref, isIntersecting } = useIntersectionObserver({ threshold: 0.1, once: true });

  return (
    <div ref={ref}>
      {isIntersecting ? children : <div style={{ minHeight: 400 }} />}
    </div>
  );
};

SEO и мета-теги

Для SPA используется react-helmet-async:

npm install react-helmet-async
import { Helmet } from 'react-helmet-async';

const HomePage = () => (
  <>
    <Helmet>
      <title>Разработка сайтов под ключ — Company Name</title>
      <meta name="description" content="Создаём сайты и веб-приложения..." />
      <meta property="og:title" content="Разработка сайтов — Company Name" />
      <meta property="og:description" content="..." />
      <meta property="og:image" content="https://example.com/og-home.jpg" />
      <meta property="og:type" content="website" />
      <link rel="canonical" href="https://example.com/" />
    </Helmet>
    {/* Контент страницы */}
  </>
);

Для проектов с серьёзными SEO-требованиями — переход на Next.js App Router с Server Components.

Тестирование

npm install -D vitest @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdom
// src/components/ui/__tests__/Button.test.tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from '../Button';

describe('Button', () => {
  it('отображает текст', () => {
    render(<Button>Нажми меня</Button>);
    expect(screen.getByRole('button', { name: 'Нажми меня' })).toBeInTheDocument();
  });

  it('вызывает onClick', async () => {
    const user = userEvent.setup();
    const onClick = vi.fn();
    render(<Button onClick={onClick}>Кнопка</Button>);
    await user.click(screen.getByRole('button'));
    expect(onClick).toHaveBeenCalledOnce();
  });

  it('disabled при loading', () => {
    render(<Button loading>Загрузка</Button>);
    expect(screen.getByRole('button')).toBeDisabled();
  });
});

Сроки

Этап Срок
Настройка проекта, структура, конфигурации 1–2 дня
UI Kit (Button, Input, Card, Dialog, Form) 2–3 дня
Маршрутизация и лейаут 1 день
Главная страница 2–3 дня
Внутренние страницы (О нас, Услуги, Блог) 1–2 дня каждая
Форма контактов с валидацией и API 1 день
SEO-разметка, robots.txt, sitemap 0.5 дня
Оптимизация (Lighthouse 90+) 1–2 дня
Тесты (E2E + unit для ключевых компонентов) 2–3 дня

Итого: посадочный сайт 5–7 страниц на React с нуля — 10–16 рабочих дней с учётом ревью и итераций.