Зачем нужен redux-thunk. Где его использовать, а где можно обойтись и без него

Йо-йо. Так как я сейчас работаю frontend-разработчиком и пишу свой код на react’е, мне приходится работать и с redux, чтобы хранить данные и как-то их изменять. Конечно же изменение этих данных влечёт новый рендер.

Из-за того, что данных бывает очень много и, порой, нужно сделать множество преобразований за одно действие, причём изменить данные или запросить их на сервере простого redux не хватает. Приходится пользоваться middleware redux-thunk. В этой статье я хочу поделиться своим опытом использования redux-thunk, рассказать где без него не обойтись, а где для лучшего UX его миновать.

Что такое middleware

Это какая-то функция, которая берёт текущий store, текущий action и что-то делает с этим. Более подробно лучше прочитать в документации.

Redux-thunk — это как раз такая функция, которая что-то делает со store.

Зачем использовать redux-thunk

Если вы задались этим вопросом, значит вы уже пользуетесь redux и знаете, что обычно action является простым объектом, который используется в reduser, a action creator имеет примерно такой вид:

export const setList = (array) => ({
    type: SET_LIST, payload: array
});

В данном примере данные будут переданы в переменной array, эти данные и получит reduser и передаст в store.

Пример 1

В примере выше всё очень просто, но часто мы ещё не имеем никаких данных и их нужно запросить с сервера. Тут — то нам и нужен middleware для того, чтобы вызвать dispatch в то время, когда данные уже будут получены. Тогда наш action creator примет такой вид:

export const getListFromServer = () =>{
    return (dispatch)=>{
        axios.get('/ajax/get-list').then((response)=>{
            dispatch({ type: SET_LIST, payload: response.data });
        });
    }
};

В этой функции мы явно вызываем dispatсh, в тот момент, когда мы получим данные.

Пример 2

Кроме того, что мы не имеем данных, бывает, что нужно вызвать сразу несколько action’ов. Например, вы хотите сделать ваш компонент неактивным пока идёт запрос. Вы можете сделать несколько вызовов dispatch в одном экшене.

export const setDetouchStadia = (bool = false) => ({
    type: SET_DETOUCH_STADIA, payload: bool
});

export const getListFromServer = () =>{
    return (dispatch)=>{
		dispatch(setDetouchStadia(true));
        axios.get('/ajax/get-list').then((response)=>{
            dispatch({ type: SET_LIST, payload: response.data });
        }).finally(()=>{
			dispatch(setDetouchStadia());
		});
    }
};

В данном примере, в начале, я вызываю другой action creator (setDetouchStadia), делаю запрос на сервер и в любом случает вызываю его заново.

Пример 3

Иногда за одно действие может быть вызвано огромное количество изменений. Сейчас (декабрь 2019) я работаю над плеером для музыки 8bit. Как у любого плеера у него есть пульт для управления треками и список треков, которые можно выбрать. В момент выбора трека мне нужно сделать множество действий и знать кое-какие данные из store.

Как я уже говорил redux-thunk, как и другие middleware, даёт нам доступ к store:

export const setCurrentTrack = (obj) => {
    return (dispatch, getState) => {
        .....
    }
}

Переменная getState — это функция, которая возвращает текущий экземпляр store.

Алгоритм действий, которые я решил, чтобы выполнялся при выборе трека:

  1. Установить текущее значение трека (приходит в переменной obj)
  2. Сделать плейлист неактивным
  3. Получить текущий экземпляр плеера
  4. Остановить играющий трек
  5. Загрузить трек
  6. Проиграть трек, если загружено
  7. Выдать ошибку, если не загружено
  8. Сделать плейлист активным

В итоге у меня получился вот такой action creator

export const setCurrentTrack = (obj) => {
    return (dispatch, getState) => {
        dispatch({type: CURRENT_TRACK,payload: obj});
        const state = getState(); 
        const player = state.playerData.player;
        const currentTrack = obj;
        const currentPlayingNode = player.currentPlayingNode;
        const loadFunction = playerLoadFunctionByCurrentTrack(currentTrack, player); // Возвращает функцию
        const result = loadFunction(); // Возвращает promise
        dispatch({type: TOGGLE_PLAY,payload: false});
        player.stop();
        dispatch(setDetouchStadia(true));
        result.then((resolve)=>{
            if(resolve === true){
                dispatch({type: TOGGLE_PLAY,payload: true});
                dispatch({ type: SET_CURRENT_PLAYER_EXAMPLE, payload: currentPlayingNode });
            }             
        }).catch(()=>{
            alert('setCurrentTrack Трэк не был загружен');
            dispatch({type: TOGGLE_PLAY,payload: false});
            dispatch({ type: SET_CURRENT_PLAYER_EXAMPLE, payload: null });

        }).finally(()=>{
			dispatch(setDetouchStadia(false));
		});;
    }
}

Когда не нужно использовать redux-thunk

Как может показаться, каждый раз, когда мы используем запросы к серверу, мы должны использовать его… А вот и нет.

Хороший пример у меня был совсем недавно в статье про getDerivedStateFromProps. Там говорилось об отмене подписки на уведомления. Там использовался хук реакта и state компонента для изменения состояния подписки и ожидания нового значения из store. Но можно пойти другим путём.

По сути, нам нужно было изменить в redux данные об элементе. Но если бы мы отправили запрос на сервер в action и ждали ответ сервера, то казалось бы, что наш сайт тормозит. Следовательно, нам нужно изменить данные и отослать запрос на сервер. И так

// Список автомобилей в store, я примера хватит
[
	{CarId: 1, subscribe: true},
	{CarId: 2, subscribe: true},
	{CarId: 3, subscribe: true},
	{CarId: 4, subscribe: true}
]
// action creator
export const unsubscribe = (CarId) => ({
	type: UNSUBSCRIBE,
	payload:CarId
})
// Reduser, который вызывает функцию unsubscribeFn во время нашего action и возвращает её результат
const carListReduser = (state = [], action) => {
	switch(action.type){
		case UNSUBSCRIBE:
			return unsubscribeFn(state, action.payload);
		.......
		default:
			return state;
	}
}
// Функция
export const unsubscribeFn = (state, CarId) => {
	const newState = [...state.map(car)=>{ 
		if(car.CarId === CarId){
			car.subscribe = false;
		}
		return car; 
	})];
	const sendData = new FormData();
	sendData.append('CarId', CarId );
	axios.post('/ajax/unsubscribe', sendData);
	return newState;
};

Как вы видите, здесь не подразумевалось получение важных данных с сервера. Поэтому лучше тут не использовать middleware, если оно не нужно. Вполне возможно, что у вас есть только подобные случаи в приложении и не нужно увеличивать бандл без необходимости.

Ещё пример

Иногда нам нужно изменить данные, опираясь на state, и не нужно получать весь store целиком. В этом случае тоже можно отказаться от thunk, а за место него использовать функции в самом reduser’е. Как я показал выше.

............
case UNSUBSCRIBE:
	return unsubscribeFn(state, action.payload);
..............

P.S.

Про react и redux стало на статью больше. Если вы не согласны с моим мнением, то можете написать своё в комментариях. А если согласны, то не стесняйтесь поддержать проект денежкой)