Thunks

Thunk — функция без аргументов, которая оборачивает вычисление для отложенного выполнения; в контексте Redux — функция-action creator, возвращающая функцию вместо объекта для обработки асинхронных операций.

Зачем нужно

Thunk — простейшая форма ленивых вычислений: вместо немедленного выполнения кода мы оборачиваем его в функцию и выполняем позже. В Redux это решает проблему async actions: стандартный Redux принимает только plain objects, а redux-thunk позволяет dispatch функций, которые сами делают async операции.

Где используется

  • Redux middleware: redux-thunk для async action creators
  • Ленивые вычисления и кэширование результата
  • Callback API (node-style (err, result) => {} thunks)
  • Откладывание вычислений до момента потребления

Основной контент

Простой thunk

// Thunk — функция, оборачивающая значение для ленивого вычисления
function add(x, y) {
  return  => x + y; // thunk
}

const thunk = add(3, 4);
// Вычисление не произошло!

// Вычислить позже
console.log(thunk); // 7
console.log(thunk); // 7 — можно вызывать многократно

Мемоизирующий thunk

// Thunk с кэшированием результата (вычислить ровно один раз)
function memoThunk(fn) {
  let evaluated = false;
  let result;

  return function {
    if (!evaluated) {
      result = fn;
      evaluated = true;
    }
    return result;
  };
}

const heavyCalc = memoThunk(() => {
  console.log('Тяжёлое вычисление...');
  return Array.from({ length: 1000 }, (_, i) => i).reduce((a, b) => a + b, 0);
});

console.log(heavyCalc); // 'Тяжёлое вычисление...' → 499500
console.log(heavyCalc); // → 499500 (без повторного вычисления)

Redux Thunk

// Обычный action creator — возвращает plain object
const increment = () => ({ type: 'INCREMENT' });

// Thunk action creator — возвращает функцию
const fetchUser = (userId) => async (dispatch, getState) => {
  // Можем использовать dispatch и getState
  dispatch({ type: 'FETCH_USER_REQUEST' });

  try {
    const response = await fetch(`/api/users/${userId}`);
    const user = await response.json();
    dispatch({ type: 'FETCH_USER_SUCCESS', payload: user });
  } catch (error) {
    dispatch({ type: 'FETCH_USER_FAILURE', error: error.message });
  }
};

// Использование (с подключённым redux-thunk middleware)
store.dispatch(fetchUser(42));

// Минимальная реализация redux-thunk middleware
const thunkMiddleware = (store) => (next) => (action) => {
  if (typeof action === 'function') {
    return action(store.dispatch, store.getState);
  }
  return next(action);
};

Node-style thunk

// Thunk в node-style: функция, принимающая callback
function readFileThunk(path) {
  return function(callback) {
    fs.readFile(path, 'utf8', callback);
  };
}

const readConfig = readFileThunk('./config.json');

// Вызываем позже с нужным callback
readConfig((err, data) => {
  if (err) return console.error(err);
  console.log(JSON.parse(data));
});

Thunk vs Promise

// Thunk: вычисление снаружи, передаётся функция
const getDataThunk = () => (dispatch) => {
  fetch('/api/data')
    .then(r => r.json())
    .then(data => dispatch({ type: 'SET_DATA', data }));
};

// Promise/async-await: более современный подход
const fetchData = async () => {
  const response = await fetch('/api/data');
  return response.json();
};
// В Redux — лучше использовать redux-toolkit createAsyncThunk

Частые ошибки

  • Thunk без redux-thunk middleware — dispatch функции без middleware вызывает ошибку. Убедитесь, что applyMiddleware(thunk) подключён.
  • Игнорирование возвращаемого значения — thunk action creator может возвращать Promise. dispatch(fetchUser(1)).then(...) работает, если thunk возвращает Promise.
  • Вложенные thunks с дублирующей логикой — лучше выделить async логику в отдельные функции/сервисы, чем размещать весь код в thunk.

Связанные темы

Ресурсы