← Назад к вопросам

Как использовал Redux на практике?

2.0 Middle🔥 181 комментариев
#React#State Management

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как использовал Redux на практике

Введение

Redux — это предсказуемое управление состоянием приложения. На практике Redux используется для управления глобальным состоянием (данные, которые нужны многим компонентам), но требует дополнительного кода и учебной кривой.

Когда я использовал Redux

Проект 1: Платформа подготовки к собеседованиям (подобный PrepBro)

Задача: Управлять состоянием вопросов, пользователя и UI для лучшего UX.

Архитектура Redux:

// store/types.ts
export interface Question {
  id: string;
  title: string;
  difficulty: 'easy' | 'medium' | 'hard';
  tags: string[];
  views: number;
  liked: boolean;
}

export interface QuestionsState {
  items: Question[];
  loading: boolean;
  error: string | null;
  currentPage: number;
  totalCount: number;
  selectedFilters: {
    difficulty?: string;
    tags?: string[];
    search?: string;
  };
}

export interface AuthState {
  user: User | null;
  isAuthenticated: boolean;
  loading: boolean;
}

export interface RootState {
  questions: QuestionsState;
  auth: AuthState;
  ui: UIState;
}

Reducers (чистые функции управления состоянием)

// store/slices/questionsSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

const initialState: QuestionsState = {
  items: [],
  loading: false,
  error: null,
  currentPage: 1,
  totalCount: 0,
  selectedFilters: {}
};

const questionsSlice = createSlice({
  name: 'questions',
  initialState,
  reducers: {
    // Синхронные actions
    setLoading: (state, action: PayloadAction<boolean>) => {
      state.loading = action.payload;
    },
    
    setQuestions: (state, action: PayloadAction<Question[]>) => {
      state.items = action.payload;
      state.loading = false;
    },
    
    setError: (state, action: PayloadAction<string | null>) => {
      state.error = action.payload;
      state.loading = false;
    },
    
    toggleLike: (state, action: PayloadAction<string>) => {
      const question = state.items.find(q => q.id === action.payload);
      if (question) {
        question.liked = !question.liked;
      }
    },
    
    setFilters: (state, action: PayloadAction<QuestionsState['selectedFilters']>) => {
      state.selectedFilters = action.payload;
      state.currentPage = 1; // Сброс на первую страницу при изменении фильтров
    },
    
    setCurrentPage: (state, action: PayloadAction<number>) => {
      state.currentPage = action.payload;
    }
  }
});

export const {
  setLoading,
  setQuestions,
  setError,
  toggleLike,
  setFilters,
  setCurrentPage
} = questionsSlice.actions;

export default questionsSlice.reducer;

Асинхронные Actions (Thunks)

// store/thunks/questionsThunks.ts
import { createAsyncThunk } from '@reduxjs/toolkit';

export const fetchQuestions = createAsyncThunk(
  'questions/fetchQuestions',
  async (
    params: {
      page: number;
      difficulty?: string;
      tags?: string[];
      search?: string;
    },
    { rejectWithValue }
  ) => {
    try {
      const queryString = new URLSearchParams();
      queryString.append('page', String(params.page));
      queryString.append('limit', '20');
      
      if (params.difficulty) {
        queryString.append('difficulty', params.difficulty);
      }
      if (params.tags?.length) {
        params.tags.forEach(tag => queryString.append('tags', tag));
      }
      if (params.search) {
        queryString.append('search', params.search);
      }

      const response = await fetch(
        `/api/v1/questions?${queryString.toString()}`
      );
      
      if (!response.ok) {
        return rejectWithValue('Failed to fetch questions');
      }

      const data = await response.json();
      return data;
    } catch (error) {
      return rejectWithValue('Network error');
    }
  }
);

export const likeQuestion = createAsyncThunk(
  'questions/likeQuestion',
  async (
    { questionId }: { questionId: string },
    { rejectWithValue }
  ) => {
    try {
      const response = await fetch(
        `/api/v1/questions/${questionId}/like`,
        { method: 'POST' }
      );
      
      if (!response.ok) {
        return rejectWithValue('Failed to like question');
      }
      
      return questionId;
    } catch (error) {
      return rejectWithValue('Network error');
    }
  }
);

Extra Reducers (обработка асинхронных actions)

// Добавляем в questionsSlice
const questionsSlice = createSlice({
  name: 'questions',
  initialState,
  reducers: { /* ... */ },
  extraReducers: (builder) => {
    builder
      // Обработка fetchQuestions
      .addCase(fetchQuestions.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchQuestions.fulfilled, (state, action) => {
        state.items = action.payload.items;
        state.totalCount = action.payload.total;
        state.loading = false;
      })
      .addCase(fetchQuestions.rejected, (state, action) => {
        state.error = action.payload as string;
        state.loading = false;
      })
      
      // Обработка likeQuestion
      .addCase(likeQuestion.fulfilled, (state, action) => {
        const question = state.items.find(q => q.id === action.payload);
        if (question) {
          question.liked = true;
        }
      })
      .addCase(likeQuestion.rejected, (state, action) => {
        state.error = action.payload as string;
      });
  }
});

Как я использовал это в компонентах

Селекторы (выбор данных из состояния)

// store/selectors.ts
import { RootState } from './store';

export const selectQuestions = (state: RootState) => state.questions.items;
export const selectQuestionsLoading = (state: RootState) => state.questions.loading;
export const selectQuestionsError = (state: RootState) => state.questions.error;
export const selectCurrentPage = (state: RootState) => state.questions.currentPage;
export const selectSelectedFilters = (state: RootState) => state.questions.selectedFilters;

// Мемоизированный селектор для оптимизации
import { createSelector } from '@reduxjs/toolkit';

export const selectFilteredQuestions = createSelector(
  [selectQuestions, selectSelectedFilters],
  (questions, filters) => {
    return questions.filter(q => {
      if (filters.difficulty && q.difficulty !== filters.difficulty) {
        return false;
      }
      if (filters.tags?.length && !filters.tags.some(tag => q.tags.includes(tag))) {
        return false;
      }
      return true;
    });
  }
);

Компонент списка вопросов

// components/QuestionsList.tsx
import { useDispatch, useSelector } from 'react-redux';
import { fetchQuestions, setFilters, setCurrentPage } from '@/store/slices/questionsSlice';
import {
  selectQuestions,
  selectQuestionsLoading,
  selectSelectedFilters,
  selectCurrentPage
} from '@/store/selectors';
import { AppDispatch, RootState } from '@/store/store';

export function QuestionsList() {
  const dispatch = useDispatch<AppDispatch>();
  const questions = useSelector(selectQuestions);
  const loading = useSelector(selectQuestionsLoading);
  const filters = useSelector(selectSelectedFilters);
  const page = useSelector(selectCurrentPage);

  useEffect(() => {
    // Загружаем вопросы при изменении фильтров или страницы
    dispatch(
      fetchQuestions({
        page,
        difficulty: filters.difficulty,
        tags: filters.tags,
        search: filters.search
      })
    );
  }, [dispatch, page, filters]);

  const handleFilterChange = (newFilters: typeof filters) => {
    dispatch(setFilters(newFilters)); // Автоматически сброс на страницу 1
  };

  const handlePageChange = (newPage: number) => {
    dispatch(setCurrentPage(newPage));
  };

  if (loading) {
    return <Spinner />;
  }

  return (
    <div>
      <Filters current={filters} onChange={handleFilterChange} />
      <div className="questions-grid">
        {questions.map(question => (
          <QuestionCard key={question.id} question={question} />
        ))}
      </div>
      <Pagination current={page} onChange={handlePageChange} />
    </div>
  );
}

Компонент карточки вопроса

// components/QuestionCard.tsx
import { useDispatch } from 'react-redux';
import { likeQuestion } from '@/store/thunks/questionsThunks';
import type { AppDispatch } from '@/store/store';

interface QuestionCardProps {
  question: Question;
}

export function QuestionCard({ question }: QuestionCardProps) {
  const dispatch = useDispatch<AppDispatch>();
  const [isLiking, setIsLiking] = useState(false);

  const handleLike = async () => {
    setIsLiking(true);
    try {
      await dispatch(likeQuestion({ questionId: question.id })).unwrap();
    } catch (error) {
      console.error('Failed to like:', error);
    } finally {
      setIsLiking(false);
    }
  };

  return (
    <div className="question-card">
      <h3>{question.title}</h3>
      <div className="difficulty">{question.difficulty}</div>
      <button
        onClick={handleLike}
        disabled={isLiking}
        className={question.liked ? 'liked' : ''}
      >
        {question.liked ? '♥' : '♡'} Like
      </button>
    </div>
  );
}

Store Setup

// store/store.ts
import { configureStore } from '@reduxjs/toolkit';
import questionsReducer from './slices/questionsSlice';
import authReducer from './slices/authSlice';
import uiReducer from './slices/uiSlice';

export const store = configureStore({
  reducer: {
    questions: questionsReducer,
    auth: authReducer,
    ui: uiReducer
  },
  devTools: process.env.NODE_ENV !== 'production'
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

Проблемы, которые я решил с Redux

Проблема 1: Prop Drilling (пробрасывание props через много уровней)

// ❌ БЕЗ Redux — пробрасываем через много компонентов
<App questions={questions}>
  <Page questions={questions}>
    <Section questions={questions}>
      <List questions={questions}>
        <Item question={question} />
      </List>
    </Section>
  </Page>
</App>

// ✅ С Redux — берём прямо где нужно
const Item = () => {
  const question = useSelector(selectQuestion);
  return <div>{question.title}</div>;
};

Проблема 2: Синхронизация состояния между компонентами

// ✅ Redux обеспечивает единый источник истины
// Если пользователь лайкнул вопрос в одном месте,
// это автоматически обновится везде
const liked = useSelector(state =>
  state.questions.items.find(q => q.id === '123')?.liked
);

Проблема 3: Управление асинхронными операциями

// ✅ Redux Thunks обеспечивают чистую обработку async операций
// Автоматически управляет loading, error, success состояниями

Когда я отказался от Redux

С развитием React (Context API, useReducer, новые хуки), я начал использовать Redux только для действительно глобального состояния:

// ✅ Redux для:
- Глобальная аутентификация (user, token)
- Данные, нужные в многих местах (questions, comments)
- Глобальные фильтры и настройки

// ✅ Context API или Local State для:
- Локальное UI состояние (expanded/collapsed, modals)
- Состояние формы
- Временные данные

Заключение

Redux на практике — это мощный инструмент для управления глобальным состоянием в больших приложениях. Я использовал его для:

  1. Управления данными — вопросы, пользователи, комментарии
  2. Асинхронных операций — загрузка данных с API
  3. UI состояния — фильтры, пагинация, сортировка
  4. Аутентификации — текущий пользователь, токен

Ключевые преимущества:

  • Предсказуемость (pure functions)
  • Отладка (Redux DevTools)
  • Масштабируемость
  • Time-travel debugging

Современный подход: Сейчас использую Redux Toolkit (упрощённая версия) или перехожу на альтернативы (Zustand, Jotai) для новых проектов, так как они проще и требуют меньше кода.

Как использовал Redux на практике? | PrepBro