Настройка State Management (Redux Toolkit) для React-приложения

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

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

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

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Настройка State Management (Redux Toolkit) для 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

Настройка State Management (Redux Toolkit) для React-приложения

Redux Toolkit (RTK) — официальная библиотека для написания Redux-логики. Она устраняет классические претензии к Redux: избыточный boilerplate, ручная настройка Immer, необходимость писать action creators вручную. RTK делает Redux компактным без потери предсказуемости.

RTK ≠ альтернатива Redux. RTK — это правильный способ писать Redux в 2024 году.

Чем RTK отличается от «голого» Redux

Аспект Redux (без RTK) Redux Toolkit
Создание actions { type: 'cart/ADD_ITEM', payload } вручную cartSlice.actions.addItem(payload)
Иммутабельность Вручную (spread operator) Через Immer — мутации в reducers допустимы
Thunk redux-thunk отдельно createAsyncThunk встроен
Селекторы reselect отдельно createSelector встроен
Серверные данные Самописный код RTK Query встроен
Конфигурация store 20+ строк boilerplate configureStore из одного вызова

Современная архитектура с RTK

src/
  store/
    index.ts
    hooks.ts
    middleware/
      errorMiddleware.ts
      analyticsMiddleware.ts
    features/
      auth/
        authSlice.ts
        authSelectors.ts
        authThunks.ts
      products/
        productsSlice.ts
        productsApi.ts      # RTK Query
      ui/
        uiSlice.ts

Feature-based структура: каждая доменная область — отдельная директория с slice, селекторами и API.

createSlice с Immer

// features/auth/authSlice.ts
import { createSlice, createAsyncThunk, type PayloadAction } from '@reduxjs/toolkit';

interface User {
  id: string;
  email: string;
  role: 'admin' | 'manager' | 'viewer';
  permissions: string[];
}

interface AuthState {
  user: User | null;
  token: string | null;
  status: 'idle' | 'loading' | 'authenticated' | 'error';
  error: string | null;
}

// createAsyncThunk генерирует pending/fulfilled/rejected actions автоматически
export const login = createAsyncThunk(
  'auth/login',
  async (credentials: { email: string; password: string }, { rejectWithValue }) => {
    try {
      const res = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials),
      });
      if (!res.ok) {
        const err = await res.json();
        return rejectWithValue(err.message);
      }
      return res.json() as Promise<{ user: User; token: string }>;
    } catch {
      return rejectWithValue('Ошибка сети');
    }
  }
);

export const authSlice = createSlice({
  name: 'auth',
  initialState: {
    user: null,
    token: localStorage.getItem('token'),
    status: 'idle',
    error: null,
  } satisfies AuthState,

  reducers: {
    // Immer позволяет мутировать state напрямую — под капотом всё иммутабельно
    logout(state) {
      state.user = null;
      state.token = null;
      state.status = 'idle';
      localStorage.removeItem('token');
    },

    updateProfile(state, action: PayloadAction<Partial<User>>) {
      if (state.user) {
        Object.assign(state.user, action.payload);
      }
    },
  },

  extraReducers: (builder) => {
    builder
      .addCase(login.pending, (state) => {
        state.status = 'loading';
        state.error = null;
      })
      .addCase(login.fulfilled, (state, action) => {
        state.user = action.payload.user;
        state.token = action.payload.token;
        state.status = 'authenticated';
        localStorage.setItem('token', action.payload.token);
      })
      .addCase(login.rejected, (state, action) => {
        state.status = 'error';
        state.error = action.payload as string;
      });
  },
});

export const { logout, updateProfile } = authSlice.actions;

RTK Query — API-слой без boilerplate

// features/products/productsApi.ts
import { createApi, fetchBaseQuery, retry } from '@reduxjs/toolkit/query/react';
import type { RootState } from '@/store';

const baseQuery = fetchBaseQuery({
  baseUrl: import.meta.env.VITE_API_URL,
  prepareHeaders: (headers, { getState }) => {
    const token = (getState() as RootState).auth.token;
    if (token) headers.set('Authorization', `Bearer ${token}`);
    return headers;
  },
});

// Автоматический retry с exponential backoff
const baseQueryWithRetry = retry(baseQuery, { maxRetries: 2 });

export const productsApi = createApi({
  reducerPath: 'productsApi',
  baseQuery: baseQueryWithRetry,
  tagTypes: ['Product', 'Category'],

  endpoints: (builder) => ({
    listProducts: builder.query<PaginatedResponse<Product>, ProductQuery>({
      query: (params) => ({ url: '/products', params }),
      providesTags: (result) =>
        result
          ? [...result.items.map(({ id }) => ({ type: 'Product' as const, id })), 'Product']
          : ['Product'],
      // Трансформация ответа для нормализации
      transformResponse: (raw: ApiResponse<Product[]>) => ({
        items: raw.data,
        total: raw.meta.total,
        page: raw.meta.page,
      }),
    }),

    createProduct: builder.mutation<Product, CreateProductDto>({
      query: (body) => ({ url: '/products', method: 'POST', body }),
      invalidatesTags: ['Product'],
      // Оптимистичное обновление
      async onQueryStarted(body, { dispatch, queryFulfilled }) {
        const patchResult = dispatch(
          productsApi.util.updateQueryData('listProducts', {}, (draft) => {
            draft.items.unshift({ id: 'temp', ...body, createdAt: new Date().toISOString() });
            draft.total += 1;
          })
        );
        try {
          await queryFulfilled;
        } catch {
          patchResult.undo();
        }
      },
    }),
  }),
});

export const { useListProductsQuery, useCreateProductMutation } = productsApi;

Middleware для сквозных задач

// store/middleware/errorMiddleware.ts
import type { Middleware } from '@reduxjs/toolkit';
import { isRejectedWithValue } from '@reduxjs/toolkit';
import { toast } from 'sonner';

export const errorMiddleware: Middleware = () => (next) => (action) => {
  if (isRejectedWithValue(action)) {
    const message = (action.payload as any)?.message ?? 'Неизвестная ошибка';
    toast.error(message);
  }
  return next(action);
};

// store/middleware/analyticsMiddleware.ts
import type { Middleware } from '@reduxjs/toolkit';

const TRACKED_ACTIONS = ['cart/addItem', 'auth/login/fulfilled', 'order/submit/fulfilled'];

export const analyticsMiddleware: Middleware = () => (next) => (action) => {
  if (typeof action.type === 'string' && TRACKED_ACTIONS.includes(action.type)) {
    window.gtag?.('event', action.type.replace(/\//g, '_'), {
      payload: JSON.stringify(action.payload),
    });
  }
  return next(action);
};
// store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';

export const store = configureStore({
  reducer: {
    auth: authSlice.reducer,
    cart: cartSlice.reducer,
    ui: uiSlice.reducer,
    [productsApi.reducerPath]: productsApi.reducer,
  },
  middleware: (getDefault) =>
    getDefault()
      .concat(productsApi.middleware)
      .concat(errorMiddleware)
      .concat(analyticsMiddleware),
});

// Автоматический refetch при reconnect и focus
setupListeners(store.dispatch);

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

// features/cart/cartSlice.test.ts
import { cartSlice, addItem, removeItem, selectCartTotal } from './cartSlice';

const { reducer } = cartSlice;

describe('cartSlice', () => {
  it('добавляет новый товар', () => {
    const state = reducer(undefined, addItem({ id: '1', name: 'Test', price: 100, image: '' }));
    expect(state.items).toHaveLength(1);
    expect(state.items[0].qty).toBe(1);
  });

  it('увеличивает qty для существующего товара', () => {
    const item = { id: '1', name: 'Test', price: 100, image: '' };
    const state = [addItem(item), addItem(item)].reduce(reducer, undefined);
    expect(state.items[0].qty).toBe(2);
  });

  it('корректно считает итог с купоном', () => {
    const rootState = { cart: { items: [{ id: '1', price: 200, qty: 2 }], couponDiscount: 10 } };
    expect(selectCartTotal(rootState as any)).toBe(360); // 400 - 10%
  });
});

Reducers — чистые функции, тестируются без React, без рендера, без моков.

Сроки реализации

  • Неделя 1: настройка store, feature-slices для основных доменов, типизация
  • Неделя 2: RTK Query endpoints, интеграция с компонентами, оптимистичные обновления
  • Неделя 3: middleware (error handling, analytics), мемоизированные селекторы
  • Неделя 4: unit-тесты reducers и selectors, документация соглашений по именованию actions