Redux Toolkit - современный Redux

16 июня 2026
Автор

Олег Марков

Введение

Redux — один из самых популярных инструментов управления состоянием в экосистеме React. Однако классический Redux долгое время критиковали за чрезмерное количество шаблонного кода: для каждого действия нужно было писать константу, action creator и обработчик в reducer. В больших приложениях это превращалось в сотни строк однотипного кода.

Redux Toolkit (сокращённо RTK) — это официальный, рекомендуемый командой Redux набор инструментов, который решает эти проблемы. RTK упрощает написание Redux-кода, устраняет распространённые ошибки и включает в себя всё необходимое для работы с Redux «из коробки».

В этой статье вы узнаете, как настроить стор с Redux Toolkit, создавать слайсы, обрабатывать асинхронные операции и использовать TypeScript с RTK.

Почему Redux Toolkit

Прежде чем переходить к коду, давайте разберёмся, зачем вообще нужен RTK, если есть классический Redux.

Проблемы классического Redux

// Классический Redux: много шаблонного кода

// 1. Константы для типов действий
const INCREMENT = 'counter/INCREMENT';
const DECREMENT = 'counter/DECREMENT';
const RESET = 'counter/RESET';

// 2. Action creators
const increment = () => ({ type: INCREMENT });
const decrement = () => ({ type: DECREMENT });
const reset = () => ({ type: RESET });

// 3. Reducer с иммутабельными обновлениями вручную
function counterReducer(state = { value: 0 }, action) {
  switch (action.type) {
    case INCREMENT:
      return { ...state, value: state.value + 1 };
    case DECREMENT:
      return { ...state, value: state.value - 1 };
    case RESET:
      return { ...state, value: 0 };
    default:
      return state;
  }
}

// 4. Настройка стора с middleware
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
const store = createStore(counterReducer, applyMiddleware(thunk));

Это только для одного простого счётчика. В реальных приложениях с десятками сущностей такой код становится неуправляемым.

Что даёт Redux Toolkit

// Тот же счётчик с Redux Toolkit
import { createSlice, configureStore } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: (state) => { state.value += 1; },
    decrement: (state) => { state.value -= 1; },
    reset: (state) => { state.value = 0; },
  },
});

export const { increment, decrement, reset } = counterSlice.actions;

const store = configureStore({
  reducer: { counter: counterSlice.reducer },
});

Код стал в 3 раза короче, не нужно писать константы и action creators вручную, а иммутабельность обеспечивается автоматически через Immer.

Установка

npm install @reduxjs/toolkit react-redux
# или
yarn add @reduxjs/toolkit react-redux
# или
pnpm add @reduxjs/toolkit react-redux

RTK уже включает redux, immer, redux-thunk и reselect — устанавливать их отдельно не нужно.

configureStore

configureStore — замена классическому createStore. Автоматически настраивает Redux DevTools Extension и добавляет redux-thunk middleware.

import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
import userReducer from './userSlice';
import postsReducer from './postsSlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
    user: userReducer,
    posts: postsReducer,
  },
  // middleware по умолчанию уже включает thunk
  // можно расширить дополнительными middleware
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(myCustomMiddleware),
  // DevTools включены автоматически в development
  devTools: process.env.NODE_ENV !== 'production',
});

// Типы для TypeScript
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

Подключение стора к React

import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './app/store';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <Provider store={store}>
    <App />
  </Provider>
);

createSlice

createSlice — основной инструмент RTK. Он объединяет в одном объекте начальное состояние, reducers и автоматически генерирует action creators.

Базовый пример

import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface CounterState {
  value: number;
  step: number;
}

const initialState: CounterState = {
  value: 0,
  step: 1,
};

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    // Мутирующий стиль благодаря Immer — внутри Jotai преобразуется в иммутабельное обновление
    increment(state) {
      state.value += state.step;
    },
    decrement(state) {
      state.value -= state.step;
    },
    // PayloadAction указывает тип payload
    incrementByAmount(state, action: PayloadAction<number>) {
      state.value += action.payload;
    },
    setStep(state, action: PayloadAction<number>) {
      state.step = action.payload;
    },
    reset() {
      // Возврат нового состояния вместо мутации
      return initialState;
    },
  },
});

// Экспортируем action creators
export const { increment, decrement, incrementByAmount, setStep, reset } =
  counterSlice.actions;

// Экспортируем reducer
export default counterSlice.reducer;

Immer и мутирующий стиль

RTK использует Immer «под капотом», что позволяет писать мутирующий код в reducers:

reducers: {
  // Это безопасно — Immer создаст новый объект
  addUser(state, action: PayloadAction<User>) {
    state.users.push(action.payload); // выглядит как мутация, но это не так
    state.total++;
  },
  updateUser(state, action: PayloadAction<{ id: string; changes: Partial<User> }>) {
    const user = state.users.find(u => u.id === action.payload.id);
    if (user) {
      Object.assign(user, action.payload.changes); // тоже безопасно
    }
  },
  removeUser(state, action: PayloadAction<string>) {
    const index = state.users.findIndex(u => u.id === action.payload);
    if (index !== -1) {
      state.users.splice(index, 1); // мутируем массив — OK!
    }
  },
}

createAsyncThunk

Для асинхронных операций RTK предоставляет createAsyncThunk. Он автоматически создаёт action creators для трёх состояний: pending, fulfilled, rejected.

Базовый пример

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';

interface User {
  id: number;
  name: string;
  email: string;
}

interface UsersState {
  users: User[];
  status: 'idle' | 'loading' | 'succeeded' | 'failed';
  error: string | null;
}

// Создаём async thunk
export const fetchUsers = createAsyncThunk(
  'users/fetchAll', // уникальный тип действия
  async () => {
    const response = await fetch('https://jsonplaceholder.typicode.com/users');
    if (!response.ok) {
      throw new Error('Не удалось загрузить пользователей');
    }
    return response.json() as Promise<User[]>;
  }
);

const usersSlice = createSlice({
  name: 'users',
  initialState: {
    users: [],
    status: 'idle',
    error: null,
  } as UsersState,
  reducers: {},
  // extraReducers обрабатывает действия из другихслайсов или thunk'ов
  extraReducers: (builder) => {
    builder
      .addCase(fetchUsers.pending, (state) => {
        state.status = 'loading';
        state.error = null;
      })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.users = action.payload;
      })
      .addCase(fetchUsers.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.error.message ?? 'Неизвестная ошибка';
      });
  },
});

export default usersSlice.reducer;

Использование в компоненте

import { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchUsers } from './usersSlice';
import type { RootState, AppDispatch } from './store';

function UsersList() {
  const dispatch = useDispatch<AppDispatch>();
  const { users, status, error } = useSelector(
    (state: RootState) => state.users
  );

  useEffect(() => {
    if (status === 'idle') {
      dispatch(fetchUsers());
    }
  }, [status, dispatch]);

  if (status === 'loading') return <p>Загрузка...</p>;
  if (status === 'failed') return <p>Ошибка: {error}</p>;

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

Передача параметров и обработка ошибок

// Параметры и обработка ошибок API
export const fetchUserById = createAsyncThunk(
  'users/fetchById',
  async (userId: number, { rejectWithValue }) => {
    try {
      const response = await fetch(`/api/users/${userId}`);
      if (response.status === 404) {
        return rejectWithValue('Пользователь не найден');
      }
      if (!response.ok) {
        return rejectWithValue(`Ошибка сервера: ${response.status}`);
      }
      return await response.json();
    } catch (error) {
      return rejectWithValue('Сетевая ошибка');
    }
  }
);

// Обработка в extraReducers
.addCase(fetchUserById.rejected, (state, action) => {
  state.status = 'failed';
  // action.payload содержит значение из rejectWithValue
  state.error = action.payload as string;
})

Отмена запросов

export const fetchData = createAsyncThunk(
  'data/fetch',
  async (_, { signal }) => {
    const response = await fetch('/api/data', { signal });
    return response.json();
  }
);

// В компоненте
useEffect(() => {
  const promise = dispatch(fetchData());

  return () => {
    // Отменяем запрос при размонтировании компонента
    promise.abort();
  };
}, [dispatch]);

Типизированные хуки

Для удобной работы с TypeScript рекомендуется создавать типизированные версии useSelector и useDispatch:

// src/app/hooks.ts
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from './store';

// Используйте эти хуки вместо стандартных во всём приложении
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// В компоненте
import { useAppDispatch, useAppSelector } from '../app/hooks';

function Counter() {
  const dispatch = useAppDispatch(); // типизированный dispatch
  const count = useAppSelector((state) => state.counter.value); // тип выводится автоматически

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => dispatch(increment())}>+</button>
    </div>
  );
}

createSelector и мемоизация

RTK реэкспортирует createSelector из библиотеки Reselect для создания мемоизированных селекторов:

import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from './store';

// Базовые селекторы
const selectAllTodos = (state: RootState) => state.todos.items;
const selectFilter = (state: RootState) => state.todos.filter;

// Мемоизированный производный селектор
// Пересчитывается только если изменились items или filter
export const selectFilteredTodos = createSelector(
  [selectAllTodos, selectFilter],
  (todos, filter) => {
    switch (filter) {
      case 'active':
        return todos.filter((t) => !t.completed);
      case 'completed':
        return todos.filter((t) => t.completed);
      default:
        return todos;
    }
  }
);

// Селектор с параметром
export const selectTodoById = createSelector(
  [selectAllTodos, (_state: RootState, id: string) => id],
  (todos, id) => todos.find((t) => t.id === id)
);

createEntityAdapter

Для работы со списками сущностей RTK предоставляет createEntityAdapter — набор готовых операций CRUD:

import { createEntityAdapter, createSlice, createAsyncThunk } from '@reduxjs/toolkit';

interface Post {
  id: string;
  title: string;
  body: string;
  authorId: string;
}

// Создаём адаптер
const postsAdapter = createEntityAdapter<Post>({
  // Опциональная сортировка
  sortComparer: (a, b) => a.title.localeCompare(b.title),
});

// Адаптер генерирует начальное состояние { ids: [], entities: {} }
const initialState = postsAdapter.getInitialState({
  status: 'idle' as 'idle' | 'loading' | 'succeeded' | 'failed',
  error: null as string | null,
});

export const fetchPosts = createAsyncThunk('posts/fetchAll', async () => {
  const response = await fetch('/api/posts');
  return response.json() as Promise<Post[]>;
});

const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {
    // Готовые CRUD-операции от адаптера
    postAdded: postsAdapter.addOne,
    postUpdated: postsAdapter.updateOne,
    postRemoved: postsAdapter.removeOne,
    postsCleared: postsAdapter.removeAll,
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchPosts.fulfilled, (state, action) => {
        state.status = 'succeeded';
        // Устанавливаем все загруженные посты
        postsAdapter.setAll(state, action.payload);
      });
  },
});

// Адаптер генерирует селекторы
export const {
  selectAll: selectAllPosts,
  selectById: selectPostById,
  selectIds: selectPostIds,
  selectTotal: selectTotalPosts,
} = postsAdapter.getSelectors((state: RootState) => state.posts);

export const { postAdded, postUpdated, postRemoved } = postsSlice.actions;
export default postsSlice.reducer;

Практический пример: корзина покупок

Соберём реальный пример — приложение с корзиной покупок:

// features/cart/cartSlice.ts
import { createSlice, createSelector, PayloadAction } from '@reduxjs/toolkit';
import type { RootState } from '../../app/store';

interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

interface CartState {
  items: CartItem[];
  isOpen: boolean;
  promoCode: string | null;
  discount: number;
}

const initialState: CartState = {
  items: [],
  isOpen: false,
  promoCode: null,
  discount: 0,
};

const cartSlice = createSlice({
  name: 'cart',
  initialState,
  reducers: {
    addItem(state, action: PayloadAction<Omit<CartItem, 'quantity'>>) {
      const existingItem = state.items.find((i) => i.id === action.payload.id);
      if (existingItem) {
        existingItem.quantity++;
      } else {
        state.items.push({ ...action.payload, quantity: 1 });
      }
    },
    removeItem(state, action: PayloadAction<string>) {
      state.items = state.items.filter((i) => i.id !== action.payload);
    },
    updateQuantity(
      state,
      action: PayloadAction<{ id: string; quantity: number }>
    ) {
      const item = state.items.find((i) => i.id === action.payload.id);
      if (item) {
        if (action.payload.quantity <= 0) {
          state.items = state.items.filter((i) => i.id !== action.payload.id);
        } else {
          item.quantity = action.payload.quantity;
        }
      }
    },
    clearCart(state) {
      state.items = [];
      state.promoCode = null;
      state.discount = 0;
    },
    toggleCart(state) {
      state.isOpen = !state.isOpen;
    },
    applyPromo(state, action: PayloadAction<string>) {
      const promoCodes: Record<string, number> = {
        SAVE10: 0.1,
        SAVE20: 0.2,
        HALFOFF: 0.5,
      };
      const discount = promoCodes[action.payload.toUpperCase()];
      if (discount) {
        state.promoCode = action.payload;
        state.discount = discount;
      }
    },
  },
});

// Селекторы
const selectCartItems = (state: RootState) => state.cart.items;
const selectDiscount = (state: RootState) => state.cart.discount;

export const selectCartTotal = createSelector(
  [selectCartItems, selectDiscount],
  (items, discount) => {
    const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
    return subtotal * (1 - discount);
  }
);

export const selectCartItemCount = createSelector(
  [selectCartItems],
  (items) => items.reduce((sum, item) => sum + item.quantity, 0)
);

export const {
  addItem,
  removeItem,
  updateQuantity,
  clearCart,
  toggleCart,
  applyPromo,
} = cartSlice.actions;

export default cartSlice.reducer;
// features/cart/CartButton.tsx
import { useAppDispatch, useAppSelector } from '../../app/hooks';
import { toggleCart, selectCartItemCount } from './cartSlice';

function CartButton() {
  const dispatch = useAppDispatch();
  const itemCount = useAppSelector(selectCartItemCount);

  return (
    <button onClick={() => dispatch(toggleCart())}>
      Корзина {itemCount > 0 && <span>({itemCount})</span>}
    </button>
  );
}

Middleware

RTK позволяет добавлять и настраивать middleware в configureStore:

import { configureStore } from '@reduxjs/toolkit';

// Кастомный logger middleware
const loggerMiddleware = (storeAPI) => (next) => (action) => {
  console.group(action.type);
  console.log('Dispatched:', action);
  const result = next(action);
  console.log('Next state:', storeAPI.getState());
  console.groupEnd();
  return result;
};

export const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      // Настройка встроенной проверки сериализации
      serializableCheck: {
        ignoredActions: ['auth/setToken'],
        ignoredPaths: ['auth.expiresAt'],
      },
    }).concat(loggerMiddleware),
});

Структура проекта с RTK

Redux Toolkit рекомендует Feature-based структуру (также называемую «ducks» или «slice» паттерном):

src/
├── app/
│   ├── store.ts          # configureStore
│   └── hooks.ts          # useAppDispatch, useAppSelector
├── features/
│   ├── counter/
│   │   ├── counterSlice.ts
│   │   ├── Counter.tsx
│   │   └── counterAPI.ts   # API-функции
│   ├── users/
│   │   ├── usersSlice.ts
│   │   ├── UsersList.tsx
│   │   └── usersAPI.ts
│   └── posts/
│       ├── postsSlice.ts
│       └── PostsList.tsx
└── App.tsx

Каждая «фича» (feature) содержит всё, что нужно для работы: слайс, компоненты и API-функции.

Сравнение Redux Toolkit с другими подходами

Аспект Redux Toolkit Zustand Jotai
Концепция Централизованный стор Сторы по необходимости Атомы
Размер ~40 КБ ~3 КБ ~3 КБ
Шаблонный код Средний (меньше чем классический Redux) Минимальный Минимальный
DevTools Полная поддержка Ограниченная jotai-devtools
Async createAsyncThunk Вручную или с middleware Нативный Suspense
TypeScript Отличный Хороший Отличный
SSR Поддерживается Поддерживается Поддерживается
Кривая обучения Средняя Низкая Низкая

RTK лучше всего подходит для:

  • Крупных приложений с командой разработчиков
  • Когда важна предсказуемость и отладка
  • Проектов, уже использующих Redux
  • Случаев, когда нужна мощь Redux DevTools

Заключение

Redux Toolkit — это правильный способ писать Redux-код в 2024 году. Он устраняет главные болевые точки классического Redux:

  • createSlice автоматически создаёт action creators и типы действий
  • Immer позволяет писать мутирующий код без потери иммутабельности
  • createAsyncThunk стандартизирует обработку асинхронных операций
  • createEntityAdapter упрощает работу со списками сущностей
  • createSelector обеспечивает мемоизированные производные данные
  • configureStore настраивает DevTools и middleware автоматически

Если вы использовали классический Redux и откладывали переход на RTK — сейчас самое время. Миграция не ломает существующий код, а новые возможности сразу же улучшат Developer Experience.

Стрелочка влевоRTK Query - работа с APIRecoil — библиотека управления состоянием от FacebookСтрелочка вправо

Постройте личный план изучения React до уровня Middle — бесплатно!

React — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по React

Uncontrolled Components: когда DOM управляет даннымиБезопасность в React: защита от XSS, CSRF и утечек данныхRender Props: гибкое управление рендерингом в ReactРефакторинг React-кода: техники и лучшие практикиПрофилирование React: как найти и устранить узкие местаЧастичное применение: как создавать компоненты без лишнего кодаИменование компонентов в React: соглашения и лучшие практикиЛенивая загрузка: как ускорить React-приложение в разыHOC в React: мастерство композиции компонентовuseMemo: как спасти производительность от тяжелых вычисленийError Boundaries: создаем надежные React-приложенияКонтролируемые компоненты в React: полный контроль над формамиCompound Components в React: создаем гибкие компоненты с мощным APIДокументирование компонентов в React: Storybook, JSDoc и READMEКомпозиция компонентов в React: строим гибкие интерфейсыКомментирование кода в React: когда и как писать комментарииCode Splitting в React: как уменьшить бандл и ускорить загрузку приложенияАсинхронные компоненты в React: новый стандарт работы с даннымиДоступность (a11y) в React: ARIA, семантика и клавиатурная навигация
Zustand — управление состоянием в ReactZod - валидация с TypeScriptYup - валидация схемXState - конечные автоматыТемизация в ReactТестирование хуковTailwind CSS с ReactSWR - библиотека для запросовStyled Components — стилизация через JSStorybook - документация компонентовSnapshots тестированиеRTK Query - работа с APIRedux Toolkit - современный ReduxRecoil — библиотека управления состоянием от FacebookВиртуализация списков с react-window: как отображать тысячи элементов без лаговReact Toastify - уведомления в ReactReact Testing LibraryСоздание таблиц в React гайд по react-tableReact Spring - анимацииРабота с формами и селектами в ReactReact Query (TanStack Query) - работа с серверомПлагины в React что это и как их использоватьReact PDF - работа с PDF файламиОбзор популярных библиотек для ReactReact Icons - библиотека иконок для ReactReact Hook Form — валидация форм в ReactReact Dropzone — загрузка файловПодключение Bootstrap к React-приложениюReact Beautiful DnD - перетаскивание элементовАнимация при монтировании компонентов в ReactМокирование APIMobX — реактивное управление состоянием в ReactМикрофронтенды с React (micro-frontends)Загрузка и индикаторыАнимация списков в ReactJotai - атомарное состояниеБесконечная прокруткаFramer Motion - библиотека анимацийEmotion — библиотека CSS-in-JSДинамические стили в ReactE2E тестирование с CypressCSSTransition - переходыCSS-in-JS — плюсы и минусыКонтекст vs Redux — когда что использоватьИспользование Chart.js в ReactAxios с ReactТестирование асинхронных компонентовОбработка ошибок API
useState в React что это и как использоватьuseTransition - плавные переходы между состояниямиuseSyncExternalStore — работа с внешними сторамиuseRef в React — создание ссылок на DOM и значенияuseOptimistic — оптимистичные обновления UIuseLayoutEffect в React — эффект до отрисовкиuseInsertionEffect — внедрение стилей до мутаций DOMuseImperativeHandle в React — настройка ref дочернего компонентаuseId — генерация уникальных идентификаторовuseFormStatus - отслеживание статуса отправки формыuseDeferredValue — отложенное обновление состоянияuseDebugValue — отладка кастомных хуковuseCallback в React — мемоизация функцийuseReducer — альтернатива useState для сложной логикиuseMemo в React: как и когда оптимизировать тяжелые вычисленияuseEffect в React что это и как использоватьuseContext — работа с контекстом в ReactuseCallback в React — мемоизация функций и оптимизация ре-рендеровuseActionState в React 19Оптимизация рендеринга в React: от теории к глубокой практикеЧто такое useRef и как его применять в ReactКак и зачем использовать React HooksУправление состоянием в React через ContextКак предотвратить лишние ре-рендеры в React: полное руководствоuseMemo vs useCallback: подробное руководство по мемоизации в ReactПравила хуков — правила использованияuseEffect vs useLayoutEffect: в чём разница и какой хук выбрать?Кастомные хуки в React — создание собственных хуковuseState продвинутое использование в React
Transition API — плавные обновления интерфейса в ReactReact Suspense — приостановка рендераStrictMode в React — как находить ошибки на этапе разработкиСерверные компоненты React (RSC) — подробный разбор и практикаКак работает рендеринг в ReactЧто такое props в React и как их правильно использоватьКак работает JSX связка React и HTMLЧто такое React.js и как его использоватьКак использовать элементы в ReactКак использовать React DOM в проектеЧто такое компоненты в React и как их применятьРабота с children в ReactПорталы в React: рендер компонентов вне иерархии DOMFragment в React: группировка элементов без лишних узлов DOMCSS Modules в ReactConcurrent Mode — конкурентный режим в React
Открыть базу знаний

Лучшие курсы по теме

изображение курса

React и Redux Toolkit

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий