Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Управление состоянием в Redux
Это классический вопрос. Redux эволюционировал, и в современных версиях есть несколько подходов.
Основной принцип Redux
Redux работает по схеме:
Action -> Reducer -> New State -> Component renders
Данные НЕ изменяются прямо. Вместо этого:
- Диспатч action
- Reducer создаёт новое состояние
- React перерендеривает компоненты
Вариант 1: Redux + Redux Thunk (классический подход)
// store/questionsSlice.js (или использовать Redux Toolkit)
import { createSlice } from '@reduxjs/toolkit';
const questionsSlice = createSlice({
name: 'questions',
initialState: {
items: [],
loading: false,
error: null
},
reducers: {
// Синхронные actions
setQuestions: (state, action) => {
state.items = action.payload;
},
addQuestion: (state, action) => {
state.items.push(action.payload);
},
deleteQuestion: (state, action) => {
state.items = state.items.filter(q => q.id !== action.payload);
},
updateQuestion: (state, action) => {
const index = state.items.findIndex(q => q.id === action.payload.id);
if (index > -1) {
state.items[index] = action.payload;
}
}
},
extraReducers: (builder) => {
// Асинхронные actions
builder
.addCase(fetchQuestions.pending, (state) => {
state.loading = true;
})
.addCase(fetchQuestions.fulfilled, (state, action) => {
state.loading = false;
state.items = action.payload;
state.error = null;
})
.addCase(fetchQuestions.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
}
});
export const { setQuestions, addQuestion, deleteQuestion, updateQuestion } = questionsSlice.actions;
export default questionsSlice.reducer;
Асинхронный thunk:
import { createAsyncThunk } from '@reduxjs/toolkit';
export const fetchQuestions = createAsyncThunk(
'questions/fetchQuestions',
async (filters, { rejectWithValue }) => {
try {
const response = await fetch('/api/v1/questions');
if (!response.ok) throw new Error('Failed to fetch');
return await response.json();
} catch (error) {
return rejectWithValue(error.message);
}
}
);
Использование в компоненте:
import { useDispatch, useSelector } from 'react-redux';
import { fetchQuestions, addQuestion } from './store/questionsSlice';
export function QuestionsList() {
const dispatch = useDispatch();
const { items, loading, error } = useSelector(state => state.questions);
useEffect(() => {
dispatch(fetchQuestions());
}, [dispatch]);
const handleAdd = (newQuestion) => {
dispatch(addQuestion(newQuestion));
};
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
{items.map(q => <QuestionCard key={q.id} question={q} />)}
</div>
);
}
Вариант 2: Zustand (современный, простой)
Mногие проекты переходят на Zustand как альтернативу Redux.
// store/questionsStore.ts
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
interface Question {
id: string;
title: string;
}
interface QuestionsStore {
items: Question[];
loading: boolean;
error: string | null;
// Actions
fetchQuestions: () => Promise<void>;
addQuestion: (q: Question) => void;
deleteQuestion: (id: string) => void;
updateQuestion: (q: Question) => void;
}
export const useQuestionsStore = create<QuestionsStore>()(
immer((set) => ({
items: [],
loading: false,
error: null,
fetchQuestions: async () => {
set((state) => { state.loading = true; });
try {
const response = await fetch('/api/v1/questions');
const data = await response.json();
set((state) => {
state.items = data;
state.loading = false;
state.error = null;
});
} catch (error) {
set((state) => {
state.loading = false;
state.error = error instanceof Error ? error.message : 'Unknown error';
});
}
},
addQuestion: (q) => {
set((state) => {
state.items.push(q);
});
},
deleteQuestion: (id) => {
set((state) => {
state.items = state.items.filter(item => item.id !== id);
});
},
updateQuestion: (q) => {
set((state) => {
const index = state.items.findIndex(item => item.id === q.id);
if (index > -1) {
state.items[index] = q;
}
});
}
}))
);
Использование в компоненте:
export function QuestionsList() {
const { items, loading, error, fetchQuestions, addQuestion } =
useQuestionsStore();
useEffect(() => {
fetchQuestions();
}, [fetchQuestions]);
const handleAdd = (newQuestion) => {
addQuestion(newQuestion);
};
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
{items.map(q => <QuestionCard key={q.id} question={q} />)}
</div>
);
}
Вариант 3: React Context + useReducer (для простых случаев)
Для небольших приложений без Redux.
// context/QuestionsContext.tsx
interface QuestionsContextType {
state: QuestionsState;
dispatch: React.Dispatch<QuestionsAction>;
}
const QuestionsContext = createContext<QuestionsContextType | null>(null);
interface QuestionsState {
items: Question[];
loading: boolean;
error: string | null;
}
type QuestionsAction =
| { type: 'FETCH_START' }
| { type: 'FETCH_SUCCESS'; payload: Question[] }
| { type: 'FETCH_ERROR'; payload: string }
| { type: 'ADD_QUESTION'; payload: Question }
| { type: 'DELETE_QUESTION'; payload: string }
| { type: 'UPDATE_QUESTION'; payload: Question };
const initialState: QuestionsState = {
items: [],
loading: false,
error: null
};
function questionsReducer(
state: QuestionsState,
action: QuestionsAction
): QuestionsState {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true, error: null };
case 'FETCH_SUCCESS':
return { ...state, items: action.payload, loading: false };
case 'FETCH_ERROR':
return { ...state, error: action.payload, loading: false };
case 'ADD_QUESTION':
return { ...state, items: [...state.items, action.payload] };
case 'DELETE_QUESTION':
return {
...state,
items: state.items.filter(q => q.id !== action.payload)
};
case 'UPDATE_QUESTION':
return {
...state,
items: state.items.map(q =>
q.id === action.payload.id ? action.payload : q
)
};
default:
return state;
}
}
export function QuestionsProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(questionsReducer, initialState);
return (
<QuestionsContext.Provider value={{ state, dispatch }}>
{children}
</QuestionsContext.Provider>
);
}
export function useQuestions() {
const context = useContext(QuestionsContext);
if (!context) {
throw new Error('useQuestions must be used within QuestionsProvider');
}
return context;
}
Использование:
export function QuestionsList() {
const { state, dispatch } = useQuestions();
useEffect(() => {
dispatch({ type: 'FETCH_START' });
fetch('/api/v1/questions')
.then(r => r.json())
.then(data => dispatch({ type: 'FETCH_SUCCESS', payload: data }))
.catch(err => dispatch({ type: 'FETCH_ERROR', payload: err.message }));
}, [dispatch]);
const { items, loading, error } = state;
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
{items.map(q => <QuestionCard key={q.id} question={q} />)}
</div>
);
}
Сравнение подходов
| Подход | Сложность | Бойлерплейт | Производительность | Когда использовать |
|---|---|---|---|---|
| Redux | Высокая | Много | Отличная | Большие приложения |
| Zustand | Средняя | Минимум | Отличная | Средние приложения |
| Context | Низкая | Среднее | Хорошая | Маленькие приложения |
Правила Redux
- Immutability: Никогда не меняй state напрямую
// Плохо
state.items.push(newItem);
// Хорошо
state.items = [...state.items, newItem];
- Чистые reducers: No side effects
// Плохо: side effect в reducer
reducer: (state, action) => {
fetch('/api'); // Ошибка!
}
// Хорошо: использовать thunk
export const fetchData = createAsyncThunk('...', async () => {
return await fetch('/api');
});
- Нормализованное состояние: Избегай вложенной структуры
// Плохо: вложено
{
authors: [
{ id: 1, posts: [{ id: 1, title: 'Post1' }] }
]
}
// Хорошо: нормализовано
{
authors: { 1: { id: 1, postIds: [1] } },
posts: { 1: { id: 1, title: 'Post1' } }
}
Мой совет
Для новых проектов: Используй Zustand вместо Redux. Меньше бойлерплейта, такая же мощь.
Для существующих Redux проектов: Миграция на Zustand требует времени. Если Redux уже работает, оставь так.
Для маленьких приложений: Context + useReducer достаточно.
Для enterprise приложений: Redux все ещё король благодаря инструментам и экосистеме.