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

Зачем нужен Redux Thunk?

1.7 Middle🔥 251 комментариев
#State Management

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

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

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

Redux Thunk: назначение и применение

Redux Thunk — это middleware для Redux, который позволяет вам писать action creators, возвращающие функцию вместо объекта action. Это критически важно для работы с асинхронными операциями в Redux.

Проблема без Redux Thunk

Без Redux Thunk работа с асинхронными операциями становится сложной. Давайте посмотрим, почему это нужно:

// Стандартный Redux: action должен быть объектом
const action = { type: 'FETCH_USER', payload: { id: 1 } };

// Но как нам выполнить асинхронный запрос?
// Если сразу задиспатчим action, данные ещё не получены

dispatch({ type: 'FETCH_USER_START' });

// Потом fetch...
fetch('/api/user')
  .then(res => res.json())
  .then(data => {
    dispatch({ type: 'FETCH_USER_SUCCESS', payload: data });
  })
  .catch(err => {
    dispatch({ type: 'FETCH_USER_ERROR', payload: err });
  });

// Это работает, но логика разбросана по приложению

Что даёт Redux Thunk

Redux Thunk позволяет action creators возвращать функцию, которой автоматически передаются dispatch и getState. Это централизует логику асинхронных операций:

// С Redux Thunk: action creator возвращает функцию
const fetchUser = (userId) => {
  // Thunk функция получает dispatch и getState
  return async (dispatch, getState) => {
    // Начало загрузки
    dispatch({ type: 'FETCH_USER_START' });
    
    try {
      // Выполняем асинхронную операцию
      const response = await fetch(\`/api/users/\${userId}\`);
      const data = await response.json();
      
      // Успех
      dispatch({ type: 'FETCH_USER_SUCCESS', payload: data });
    } catch (error) {
      // Ошибка
      dispatch({ type: 'FETCH_USER_ERROR', payload: error.message });
    }
  };
};

// Использование
dispatch(fetchUser(123));

Главные преимущества Redux Thunk

1. Логика асинхронных операций в action creators

Все логика для работы с API находится в одном месте — в функции action creator:

// Пример: загрузка и сохранение комментария
export const saveComment = (postId, commentText) => async (dispatch, getState) => {
  dispatch({ type: 'SAVE_COMMENT_START' });
  
  try {
    const response = await fetch('/api/comments', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ postId, text: commentText })
    });
    
    const newComment = await response.json();
    dispatch({ 
      type: 'SAVE_COMMENT_SUCCESS', 
      payload: newComment 
    });
  } catch (error) {
    dispatch({ 
      type: 'SAVE_COMMENT_ERROR', 
      payload: error.message 
    });
  }
};

2. Доступ к текущему состоянию через getState

Thunk может проверить текущее состояние перед выполнением:

export const updateUserIfNotChanged = (userId, newData) => async (dispatch, getState) => {
  const state = getState();
  const currentUser = state.user;
  
  // Проверяем, не изменился ли пользователь в другой вкладке
  if (currentUser.id !== userId) {
    dispatch({ type: 'USER_CHANGED_ELSEWHERE' });
    return;
  }
  
  // Продолжаем обновление
  dispatch({ type: 'UPDATE_USER_START' });
  // ...
};

3. Условная диспетчеризация actions

Thunk позволяет диспатчить несколько actions в зависимости от условий:

export const loadUserData = (userId) => async (dispatch, getState) => {
  const state = getState();
  
  // Кэширование: если данные уже загружены, не загружаем снова
  if (state.users[userId]) {
    dispatch({ type: 'USE_CACHED_USER', payload: state.users[userId] });
    return;
  }
  
  // Новый запрос
  dispatch({ type: 'FETCH_USER_START' });
  try {
    const user = await fetch(\`/api/users/\${userId}\`).then(r => r.json());
    dispatch({ type: 'FETCH_USER_SUCCESS', payload: user });
  } catch (error) {
    dispatch({ type: 'FETCH_USER_ERROR', payload: error });
  }
};

Структура Redux с Thunk

Обычная архитектура выглядит так:

// actions.js
export const fetchPosts = () => async (dispatch) => {
  dispatch({ type: 'FETCH_POSTS_START' });
  try {
    const posts = await fetch('/api/posts').then(r => r.json());
    dispatch({ type: 'FETCH_POSTS_SUCCESS', payload: posts });
  } catch (error) {
    dispatch({ type: 'FETCH_POSTS_ERROR', payload: error.message });
  }
};

// reducer.js
const initialState = {
  posts: [],
  loading: false,
  error: null
};

export function postsReducer(state = initialState, action) {
  switch (action.type) {
    case 'FETCH_POSTS_START':
      return { ...state, loading: true, error: null };
    case 'FETCH_POSTS_SUCCESS':
      return { ...state, posts: action.payload, loading: false };
    case 'FETCH_POSTS_ERROR':
      return { ...state, error: action.payload, loading: false };
    default:
      return state;
  }
}

// store.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { postsReducer } from './reducer';

const store = createStore(postsReducer, applyMiddleware(thunk));

// App.js
function App() {
  const dispatch = useDispatch();
  const { posts, loading, error } = useSelector(state => state);
  
  useEffect(() => {
    dispatch(fetchPosts());
  }, [dispatch]);
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  return <div>{posts.map(p => <p key={p.id}>{p.title}</p>)}</div>;
}

Redux Thunk vs другие подходы

vs Обычные action creators

// Без Thunk: логика в компоненте (плохо)
function UserProfile({ userId }) {
  useEffect(() => {
    dispatch({ type: 'FETCH_USER_START' });
    fetch(\`/api/users/\${userId}\`)
      .then(r => r.json())
      .then(data => dispatch({ type: 'FETCH_USER_SUCCESS', payload: data }))
      .catch(err => dispatch({ type: 'FETCH_USER_ERROR', payload: err }));
  }, [userId]);
}

// С Thunk: логика в action creator (хорошо)
function UserProfile({ userId }) {
  useEffect(() => {
    dispatch(fetchUser(userId));
  }, [userId, dispatch]);
}

vs Redux-Saga

Redux Thunk:

  • Простой и лёгкий в использовании
  • Достаточен для большинства случаев
  • Меньше boilerplate кода

Redux-Saga:

  • Более мощный и гибкий
  • Для сложных асинхронных потоков
  • Больше boilerplate кода
// Redux Thunk: простой запрос
export const fetchUser = (id) => async (dispatch) => {
  const user = await fetch(\`/api/users/\${id}\`).then(r => r.json());
  dispatch({ type: 'SET_USER', payload: user });
};

// Redux-Saga: более сложный сценарий с побочными эффектами
function* watchFetchUser() {
  yield takeEvery('FETCH_USER_REQUEST', fetchUserSaga);
}

function* fetchUserSaga(action) {
  try {
    const user = yield call(fetch, \`/api/users/\${action.id}\`);
    yield put({ type: 'FETCH_USER_SUCCESS', payload: user });
  } catch (e) {
    yield put({ type: 'FETCH_USER_ERROR', payload: e.message });
  }
}

Типичные ошибки при использовании Thunk

// ❌ Забыли вернуть функцию
export const badThunk = (data) => {
  // Это не Thunk! Это обычный action creator
  return { type: 'ACTION', payload: data };
};

// ✅ Правильно: возвращаем функцию
export const goodThunk = (data) => async (dispatch) => {
  dispatch({ type: 'START' });
  const result = await someAsync();
  dispatch({ type: 'SUCCESS', payload: result });
};

// ❌ Забыли middleware
const store = createStore(reducer); // Redux Thunk не будет работать!

// ✅ Правильно: applyMiddleware
const store = createStore(reducer, applyMiddleware(thunk));

// ❌ Использовали Thunk в reducer
function reducer(state, action) {
  if (typeof action === 'function') { // Никогда так не делайте!
    action(dispatch, getState);
  }
}

// ✅ Redux Thunk автоматически обрабатывает функции

Альтернативы в современном React

Если вы используете современный React без Redux, есть более простые подходы:

// С React Hooks + Context (без Redux)
function useUser(userId) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    setLoading(true);
    fetch(\`/api/users/\${userId}\`)
      .then(r => r.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err);
        setLoading(false);
      });
  }, [userId]);
  
  return { user, loading, error };
}

// Или с Tanstack Query (formerly React Query)
function useUser(userId) {
  return useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch(\`/api/users/\${userId}\`).then(r => r.json())
  });
}

Итоговый вывод

Redux Thunk нужен для:

  1. Обработки асинхронных операций в Redux приложениях
  2. Централизации логики асинхронных запросов в action creators
  3. Доступа к текущему состоянию перед выполнением операции
  4. Условной диспетчеризации multiple actions

Redux Thunk остаётся популярным, потому что:

  • Очень простой в использовании
  • Не требует дополнительной конфигурации
  • Работает для 90% use cases
  • Минимальный overhead

Однако, если вы начинаете новый проект с React, рассмотрите React Hooks + Tanstack Query или другие современные решения вместо Redux.