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

Как изменить данные в Redux?

1.0 Junior🔥 111 комментариев
#State Management

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

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

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

Управление состоянием в Redux

Это классический вопрос. Redux эволюционировал, и в современных версиях есть несколько подходов.

Основной принцип Redux

Redux работает по схеме:

Action -> Reducer -> New State -> Component renders

Данные НЕ изменяются прямо. Вместо этого:

  1. Диспатч action
  2. Reducer создаёт новое состояние
  3. 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

  1. Immutability: Никогда не меняй state напрямую
// Плохо
state.items.push(newItem);

// Хорошо
state.items = [...state.items, newItem];
  1. Чистые reducers: No side effects
// Плохо: side effect в reducer
reducer: (state, action) => {
  fetch('/api'); // Ошибка!
}

// Хорошо: использовать thunk
export const fetchData = createAsyncThunk('...', async () => {
  return await fetch('/api');
});
  1. Нормализованное состояние: Избегай вложенной структуры
// Плохо: вложено
{
  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 все ещё король благодаря инструментам и экосистеме.