Как использовал Redux на практике?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как использовал 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 на практике — это мощный инструмент для управления глобальным состоянием в больших приложениях. Я использовал его для:
- Управления данными — вопросы, пользователи, комментарии
- Асинхронных операций — загрузка данных с API
- UI состояния — фильтры, пагинация, сортировка
- Аутентификации — текущий пользователь, токен
Ключевые преимущества:
- Предсказуемость (pure functions)
- Отладка (Redux DevTools)
- Масштабируемость
- Time-travel debugging
Современный подход: Сейчас использую Redux Toolkit (упрощённая версия) или перехожу на альтернативы (Zustand, Jotai) для новых проектов, так как они проще и требуют меньше кода.