April 10, 2021

Redux — для чего он нужен

Все взаимодействие нашего приложения происходит в интерфейсе. Но хочется отделить бизнес-логику, чтобы было проще разрабатывать и дебажить.

Одна из архитектур для работы с данными Flux. Мы делаем действие из интерфейса, в действии читаем логику и обновляем состояние. А интерфейс всегда отображает текущее состояние.

Вот названия по-научному.

И картинка из интернета

Redux — это реализация архитектуры Flux.

Мы создаем store с помощью функции createStore

const store = createStore(reducer, middlewares);

reducer — это функция, которая обновляет стор в зависимости от экшена.

middlewares — это промежуточные слои, прежде чем экшн дойдет до редьюсера.

Дальше стор можно диспатчить и смотреть текущее состояние

// В диспатч обязательно надо передать type, тип экшена,
// чтобы редьюсер мог обновить состояние по этому типу
// payload — это дополнительные данные
store.dispatch({ type: 'myAction', payload: { s: 2 } });
store.getState(); // Вернет текущее состояние

В редьюсере важно всегда возвращать новый объект, чтобы не было неочевидных багов, т.к. интерфейс отображает текущее состояние. После того, как новое состояние вернется из редьюсера, реакт обновится.

const reducer = (state = {}, { type, payload }) => {
  switch (type) {
    case 'myAction': {
      return {
        ...state,
        result: 2 + payload.s,
      };
    }
 
    default: {
      return state;
    }
  }
};

Редакс подключается к реакту также, как мы подключаем контекст

import { Provider } from 'react-redux';
import { createStore } from 'redux';
 
const store = createStore(() => ({}));
 
const App = () => (
  <Provider store={store}>
    <h1>Привет, это 0xLDev</h1>
  </Provider>
);

Чтобы использовать состояние или диспатчить экшены, используем хуки

import { useDispatch, useSelector } from 'redux';
import { getFunc } from './actions';
 
const Component = () => {
  const dispatch = useDispatch();
  const name = useSelector((state) => state.name);
 
  return (
    <button onClick={() => dispatch(getFunc)}>{name}, нажми на меня!</button>
  );
};

Для асинхронных запросов в экшенах используем библиотеку redux-thunk. Она позволяет не возвращать объект из экшена сразу, а задиспатчить его отдельно. Причем несколько раз.

export const getFunc = () => (dispatch, getState) => {
  const state = getState();  // Посмотреть состояние для валидации, например. 
  dispatch({ type: 'fetchStart' });
  fetch(url)
    .then(() => dispatch({ type: 'fetchSuccess' }))
    .catch(() => dispatch({ type: 'fetchFail' }))
};

redux-thunk работает через middleware. Подключаем к стору так

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
 
const store = createStore(reducer, applyMiddleware(thunk));

MiddleWare — это функция, которая вызовется перед попаданием в редьюсер. Мы пишем свои middleware для логирования. Внутри он выглядит так

const middleware = (store) => (next) => (action) => {
  // ...тут делаем все, что нам нужно
  
  // когда будем готовы передать экшн дальше
  // в редьюсер или следующий по порядку миддлвэр
  // пишем так. Вызвать можно только один раз
  next(action);
};

Самый кайф редакса в логировании бизнес-логики. И redux-devtools-extension помогает удобно смотреть изменения.