image
В настоящее время разработка львиной доли веб-приложений, основанных на фреймворке React, ведется с использованием библиотеки Redux. Данная библиотека является самой популярной реализацией FLUX-архитектуры и, несмотря на ряд очевидных преимуществ, имеет весьма существенные недостатки, такие как:


  • сложность и “многословность” рекомендованных паттернов для написания и организации кода, что влечет за собой большое количество бойлерплейта;
  • отсутствие встроенных средств управления асинхронным поведением и побочными эффектами, что приводит к необходимости выбора подходящего инструмента из множества аддонов, написанных сторонними разработчиками.

Для устранения этих недостатков разработчики Redux представили библиотеку Redux Toolkit. Этот инструмент представляет собой набор практических решений и методов, предназначенных для упрощения разработки приложений с использованием Redux. Разработчики данной библиотеки преследовали цель упростить типичные случаи использования Redux. Данный инструмент не является универсальным решением в каждом из возможных случаев использования Redux, но позволяет упростить тот код, который требуется написать разработчику.


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


Кратко о библиотеке


Краткая информация о Redux Toolkit:


  • до релиза библиотека называлась redux-starter-kit;
  • релиз состоялся в конце октября 2019 года;
  • библиотека официально поддерживается разработчиками Redux.

Согласно заявлению разработчиков, Redux Toolkit выполняет следующие функции:


  • помогает быстро начать использовать Redux;
  • упрощает работу с типичными задачами и кодом Redux;
  • позволяет использовать лучшие практики Redux по умолчанию;
  • предлагает решения, которые уменьшают недоверие к бойлерплейтам.

Redux Toolkit предоставляет набор как специально разработанных, так и добавляет ряд хорошо себя зарекомендовавших инструментов, которые обычно используются совместно с Redux. Такой подход позволяет разработчику решить как и какие инструменты использовать в своем приложении. По ходу данной статьи мы будем отмечать какие заимствования использует данная библиотека. Более полную информацию и зависимостях Redux Toolkit можно получить из описания пакета @reduxjs/toolkit.


Наиболее значимыми функциями, предоставляемыми библиотекой Redux Toolkit являются:


  • #configureStore — функция, предназначенная упростить процесс создания и настройки хранилища;
  • #createReducer — функция, помогающая лаконично и понятно описать и создать редьюсер;
  • #createAction — возвращает функцию создателя действия для заданной строки типа действия;
  • #createSlice — объединяет в себе функционал createAction и createReducer;
  • createSelector — функция из библиотеки Reselect, переэкспортированная для простоты использования.

Также, стоит отметить, что Redux Toolkit полностью интегрирован с TypeScript. Более подробную информацию об этом можно получить из раздела Usage With TypeScript официальной документации.


Применение


Рассмотрим использование библиотеки Redux Toolkit на примере фрагмента реально используемого React Redux приложения.
Примечание. Далее в статье будет приводиться исходный код как без использования Redux Toolkit, так и с использованием, что позволит лучше оценить положительные и отрицательные стороны использования данной библиотеки.


Задача


В одном из наших внутренних приложений возникла необходимость добавлять, редактировать и отображать информацию о релизах выпускаемых нами программных продуктов. Для каждого из этих действий были разработаны отдельные функции API, результаты выполнения которых и требуется добавлять в Redux store. В качестве средства управления асинхронным поведением и побочными эффектами будем использовать Thunk.


Создание хранилища


Первоначальный вариант исходного кода, осуществляющего создание хранилища выглядел следующим образом:


import {
  createStore, applyMiddleware, combineReducers, compose,
} from 'redux';
import thunk from 'redux-thunk';
import * as reducers from './reducers';

const ext = window.__REDUX_DEVTOOLS_EXTENSION__;
const devtoolMiddleware = 
  ext && process.env.NODE_ENV === 'development' ? ext() : f => f;

const store = createStore(
 combineReducers({
   ...reducers,
 }),
 compose(
   applyMiddleware(thunk),
   devtoolMiddleware
 )
);

Если внимательно взглянуть на приведенный код, можно увидеть довольно длинную последовательность действий, которую необходимо совершить чтобы хранилище было полностью сконфигурировано. Redux Toolkit содержит инструмент, призванный упростить данную процедуру, а именно: функцию configureStore.


Функция configureStore


Данный инструмент позволяет автоматически комбинировать редьюсеры, добавить мидлвары Redux (по умолчанию включает redux-thunk), а также использовать расширение Redux DevTools. В качестве входных параметров функция configureStore принимает объект со следующими свойствами:


  • reducer — набор пользовательских редьюсеров,
  • middleware — опциональный параметр, задающий массив мидлваров, предназначенных для подключения к хранилищу,
  • devTools — параметр логического типа, позволяющий включить установленное в браузер расширение Redux DevTools (значение по умолчанию — true),
  • preloadedState — опциональный параметр, задающий начальное состояние хранилища,
  • enhancers — опциональный параметр, задающий набор усилителей.

Для получения наиболее популярного списка мидлваров можно воспользоваться специальной функцией getDefaultMiddleware, также входящей в состав Redux Toolkit. Данная функция возвращает массив с включенными по умолчанию в библиотеку Redux Toolkit мидлварами. Перечень этих мидлваров отличается в зависимости от того, в каком режиме выполняется ваш код. В production режиме массив состоит только из одного элемента — thunk. В режиме development на момент написания статьи список пополняется следующими мидлварами:


  • serializableStateInvariant — инструмент, специально разработанный для использования в Redux Toolkit и предназначенный для проверки дерева состояний на предмет наличия несериализуемых значений, таких как функции, Promise, Symbol и другие значения, не являющиеся простыми JS-данными;
  • immutableStateInvariant — мидлвар из пакета redux-immutable-state-invariant, предназначенный для обнаружения мутаций данных, содержащихся в хранилище.

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


Теперь перепишем участок кода, отвечающий за создание хранилища, воспользовавшись описанными выше инструментами. В результате получим следующее:


import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
import * as reducers from './reducers';

const middleware = getDefaultMiddleware({
  immutableCheck: false,
  serializableCheck: false,
  thunk: true,
});

export const store = configureStore({
 reducer: { ...reducers },
 middleware,
 devTools: process.env.NODE_ENV !== 'production',
});

На примере данного участка кода хорошо видно, что функция configureStore решает следующие проблемы:


  • необходимость комбинировать редьюсеры, автоматически вызывая combineReducers,
  • необходимость комбинировать мидлвары, автоматически вызывая applyMiddleware.

А также позволяет более удобно включить расширение Redux DevTools, используя функцию composeWithDevTools из пакета redux-devtools-extension. Все вышесказанное свидетельствует о том, что использование данной функции позволяет сделать код более компактным и понятным.


На этом создание и настройка хранилища завершены. Передаем его в провайдер и переходим далее.


Действия, создатели действий и редьюсер


Теперь рассмотрим возможности Redux Toolkit в части разработки действий, создателей действий и редьюсера. Первоначальный вариант кода без использования Redux Toolkit был организован в виде файлов actions.js и reducers.js. Содержимое файла actions.js выглядело следующим образом:


import * as productReleasesService from '../../services/productReleases';

export const PRODUCT_RELEASES_FETCHING = 'PRODUCT_RELEASES_FETCHING';
export const PRODUCT_RELEASES_FETCHED = 'PRODUCT_RELEASES_FETCHED';
export const PRODUCT_RELEASES_FETCHING_ERROR =
  'PRODUCT_RELEASES_FETCHING_ERROR';

…

export const PRODUCT_RELEASE_UPDATING = 'PRODUCT_RELEASE_UPDATING';
export const PRODUCT_RELEASE_UPDATED = 'PRODUCT_RELEASE_UPDATED';
export const PRODUCT_RELEASE_CREATING_UPDATING_ERROR =
  'PRODUCT_RELEASE_CREATING_UPDATING_ERROR';

function productReleasesFetching() {
  return {
    type: PRODUCT_RELEASES_FETCHING
  };
}

function productReleasesFetched(productReleases) {
  return {
    type: PRODUCT_RELEASES_FETCHED,
    productReleases
  };
}

function productReleasesFetchingError(error) {
  return {
    type: PRODUCT_RELEASES_FETCHING_ERROR,
    error
  }
}

…

export function fetchProductReleases() {
  return dispatch => {
    dispatch(productReleasesFetching());
    return productReleasesService.getProductReleases().then(
      productReleases => dispatch(productReleasesFetched(productReleases))
    ).catch(error => {
      error.clientMessage = "Can't get product releases";
      dispatch(productReleasesFetchingError(error))
    });
  }
}

…

export function updateProductRelease(
  id, productName, productVersion, releaseDate
) {
  return dispatch => {
    dispatch(productReleaseUpdating());
    return productReleasesService.updateProductRelease(
      id, productName, productVersion, releaseDate
    ).then(
      productRelease => dispatch(productReleaseUpdated(productRelease))
    ).catch(error => {
      error.clientMessage = "Can't update product releases";
      dispatch(productReleaseCreatingUpdatingError(error))
    });
  }
}

Содержимое файла reducers.js до использования Redux Toolkit:


const initialState = {
 productReleases: [],
 loadedProductRelease: null,
 fetchingState: 'none',
 creatingState: 'none',
 updatingState: 'none',
 error: null,
};

export default function reducer(state = initialState, action = {}) {
 switch (action.type) {
   case productReleases.PRODUCT_RELEASES_FETCHING:
     return {
       ...state,
       fetchingState: 'requesting',
       error: null,
     };
   case productReleases.PRODUCT_RELEASES_FETCHED:
     return {
       ...state,
       productReleases: action.productReleases,
       fetchingState: 'success',
     };
   case productReleases.PRODUCT_RELEASES_FETCHING_ERROR:
     return {
       ...state,
       fetchingState: 'failed',
       error: action.error
     };

…

   case productReleases.PRODUCT_RELEASE_UPDATING:
     return {
       ...state,
       updatingState: 'requesting',
       error: null,
     };
   case productReleases.PRODUCT_RELEASE_UPDATED:
     return {
       ...state,
       updatingState: 'success',
       productReleases: state.productReleases.map(productRelease => {
         if (productRelease.id === action.productRelease.id)
           return action.productRelease;
         return productRelease;
       })
     };
   case productReleases.PRODUCT_RELEASE_UPDATING_ERROR:
     return {
       ...state,
       updatingState: 'failed',
       error: action.error
     };
   default:
     return state;
 }
}

Как мы можем видеть, именно здесь содержится большая часть бойлерплейта: константы типов действий, создатели действий, снова константы, но уже в коде редьюсера на написание всего этого кода приходится тратить время. Частично от этого бойлерплейта можно избавиться, если воспользоваться функциями createAction и createReducer, которые также входят в состав Redux Toolkit.


Функция createAction


В приведенном участке кода используется стандартный способ определения действия в Redux: сначала отдельно объявляется константа, определяющая тип действия, после чего — функция создателя действия этого типа. Функция createAction объединяет эти два объявления в одно. На вход она принимает тип действия и возвращает создателя действия для этого типа. Создатель действия может быть вызван либо без аргументов, либо с некоторым аргументом (полезная нагрузка), значение которого будет помещено в поле payload, созданного действия. Кроме того, создатель действия переопределяет функцию toString(), так что тип действия становится его строковым представлением.


В некоторых случаях может понадобиться написать дополнительную логику для настройки значения полезной нагрузки, например, принять несколько параметров для создателя действия, создать случайный идентификатор или получить текущую временну?ю метку. Для этого createAction принимает необязательный второй аргумент — функцию, которая будет использоваться для обновления значения полезной нагрузки. Подробнее о данном параметре можно ознакомиться в официальной документации.
Использовав функцию createAction, получим следующий код:


export const productReleasesFetching =
  createAction('PRODUCT_RELEASES_FETCHING');
export const productReleasesFetched =
  createAction('PRODUCT_RELEASES_FETCHED');
export const productReleasesFetchingError =
  createAction('PRODUCT_RELEASES_FETCHING_ERROR');

…

export function fetchProductReleases() {
  return dispatch => {
    dispatch(productReleasesFetching());
    return productReleasesService.getProductReleases().then(
      productReleases => dispatch(productReleasesFetched({ productReleases }))
    ).catch(error => {
      error.clientMessage = "Can't get product releases";
      dispatch(productReleasesFetchingError({ error }))
    });
  }
}
...

Функция createReducer


Теперь рассмотрим редьюсер. Как и в нашем примере, редьюсеры часто реализуются с помощью оператора switch, с одним регистром для каждого обработанного типа действия. Этот подход работает хорошо, но не лишен бойлерплейта и подвержен ошибкам. Например, легко забыть описать случай default или не установить начальное состояние. Функция createReducer упрощает создание функций редьюсера, определяя их как таблицы поиска функций для обработки каждого типа действия. Она также позволяет существенно упростить логику иммутабельного обновления, написав код в “мутабельном” стиле внутри редьюсеров.


“Мутабельный” стиль обработки событий доступен благодаря использованию библиотеки Immer. Функция обработчик может либо “мутировать” переданный state для изменения свойств, либо возвращать новый state, как при работе в иммутабельном стиле, но, благодаря Immer, реальная мутация объекта не осуществляется. Первый вариант куда проще для работы и восприятия, особенно при изменении объекта с глубокой вложенностью.


Будьте внимательны: возврат нового объекта из функции перекрывает “мутабельные” изменения. Одновременное применение обоих методов обновления состояния не сработает.


В качестве входных параметров функция createReducer принимает следующие аргументы:


  • начальное состояние хранилища,
  • объект, устанавливающий соответствие между типами действий и редьюсерами, каждый из которых обрабатывает какой-то определенный тип.

Воспользовавшись методом createReducer, получим следующий код:


const initialState = {
 productReleases: [],
 loadedProductRelease: null,
 fetchingState: 'none',
 creatingState: 'none',
 loadingState: 'none',
 error: null,
};

const counterReducer = createReducer(initialState, {
 [productReleasesFetching]: (state, action) => {
   state.fetchingState = 'requesting'
 },
 [productReleasesFetched.type]: (state, action) => {
   state.productReleases = action.payload.productReleases;
   state.fetchingState = 'success';
 },
 [productReleasesFetchingError]: (state, action) => {
   state.fetchingState = 'failed';
   state.error = action.payload.error;
 },

…

 [productReleaseUpdating]: (state) => {
   state.updatingState = 'requesting'
 },
 [productReleaseUpdated]: (state, action) => {
   state.updatingState = 'success';
   state.productReleases = state.productReleases.map(productRelease => {
     if (productRelease.id === action.payload.productRelease.id)
       return action.payload.productRelease;
     return productRelease;
   });
 },
 [productReleaseUpdatingError]: (state, action) => {
   state.updating = 'failed';
   state.error = action.payload.error;
 },
});

Как мы видим, использование функций createAction и createReducer существенно решает проблему написания лишнего кода, но проблема предварительного создания констант всё равно остается. Поэтому рассмотрим более мощный вариант, объединяющий в себе генерацию и создателей действий и редьюсера — функция createSlice.


Функция createSlice


В качестве входных параметров функция createSlice принимает объект со следующими полями:


  • name — пространство имен создаваемых действий (${name}/${action.type});
  • initialState — начальное состояние редьюсера;
  • reducers — объект с обработчиками. Каждый обработчик принимает функцию с аргументами state и action, action содержит в себе данные в свойстве payload и имя события в свойстве name. Кроме того, имеется возможность предварительного изменения данных, полученных из события, перед их попаданием в редьюсер (например, добавить id к элементам коллекции). Для этого вместо функции необходимо передать объект с полями reducer и prepare, где reducer — это функция-обработчик действия, а prepare — функция-обработчик полезной нагрузки, возвращающая обновленный payload;
  • extraReducers — объект, содержащий редьюсеры другого среза. Данный параметр может потребоваться в случае необходимости обновления объекта, относящегося к другому срезу. Подробнее про данную функциональную возможность можно узнать из соответствующего раздела официальной документации.

Результатом работы функции является объект, называемый "срез", со следующими полями:


  • name — имя среза,
  • reducer — редьюсер,
  • actions — набор действий.

Использовав данную функцию для решения нашей задачи, получим следующий исходный код:


const initialState = {
 productReleases: [],
 loadedProductRelease: null,
 fetchingState: 'none',
 creatingState: 'none',
 loadingState: 'none',
 error: null,
};

const productReleases = createSlice({
 name: 'productReleases',
 initialState,
 reducers: {
   productReleasesFetching: (state) => {
     state.fetchingState = 'requesting';
   },
   productReleasesFetched: (state, action) => {
     state.productReleases = action.payload.productReleases;
     state.fetchingState = 'success';
   },
   productReleasesFetchingError: (state, action) => {
     state.fetchingState = 'failed';
     state.error = action.payload.error;
   },

…

   productReleaseUpdating: (state) => {
     state.updatingState = 'requesting'
   },
   productReleaseUpdated: (state, action) => {
     state.updatingState = 'success';
     state.productReleases = state.productReleases.map(productRelease => {
       if (productRelease.id === action.payload.productRelease.id)
         return action.payload.productRelease;
       return productRelease;
     });
   },
   productReleaseUpdatingError: (state, action) => {
     state.updating = 'failed';
     state.error = action.payload.error;
   },
 },
});

Теперь извлечем из созданного среза создатели действий и редьюсер.


const { actions, reducer } = productReleases;

export const {
  productReleasesFetched, productReleasesFetching,
  productReleasesFetchingError,
…
  productReleaseUpdated,
  productReleaseUpdating, productReleaseUpdatingError
} = actions;

export default reducer;

Исходный код создателей действий, содержащих вызовы API, не изменился, за исключением способа передачи параметров при отправке действий:


export const fetchProductReleases = () => (dispatch) => {
 dispatch(productReleasesFetching());
 return productReleasesService
   .getProductReleases()
   .then((productReleases) => dispatch(productReleasesFetched({ productReleases })))
   .catch((error) => {
     error.clientMessage = "Can't get product releases";
     dispatch(productReleasesFetchingError({ error }));
   });
};

…

export const updateProductRelease = (id, productName, productVersion, releaseDate) => (dispatch) => {
 dispatch(productReleaseUpdating());
 return productReleasesService
   .updateProductRelease(id, productName, productVersion, releaseDate)
   .then((productRelease) => dispatch(productReleaseUpdated({ productRelease })))
   .catch((error) => {
     error.clientMessage = "Can't update product releases";
     dispatch(productReleaseUpdatingError({ error }));
   });

Приведенный выше код, показывает, что функция createSlice позволяет избавиться от значительной части бойлерплейта при работе с Redux, что позволяет не только сделать код более компактным, лаконичным и понятным, но и тратить меньше времени на его написание.


Итог


В завершении данной статьи, хотелось бы сказать, что несмотря на то, что библиотека Redux Toolkit не вносит ничего нового в управление хранилищем, она предоставляет ряд гораздо более удобных средств для написания кода чем были до этого. Данные средства позволяют не только сделать процесс разработки более удобным, понятным и быстрым, но и более эффективным, за счет наличия в библиотеке ряда хорошо зарекомендовавших себя ранее инструментов. Мы, в Инобитек, планируем и дальше использовать данную библиотеку при разработке наших программных продуктов и следить за новыми перспективными разработками в области Web-технологий.


Спасибо за внимание. Надеемся, что наша статья окажется полезной. Более подробную информацию о библиотеке Redux Toolkit можно получить из официальной документации.

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


  1. Alhymik
    23.12.2019 12:14

    Я конечно извиняюсь, что высказываю впечатление, случайно зайдя в тему, не являясь реакт- разработчиком, но если фреймворк приходится использовать с такой гигантской инвалидной каляской, вы уверены, что это вообще можно называть фреймворком?


    1. aleki
      23.12.2019 13:11

      Он и не называется фреймворком (ни react, ни redux), всё это фантазии автора.


    1. kolalexo Автор
      23.12.2019 16:39

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


      1. Alhymik
        23.12.2019 18:30

        Примерно понял. Интересно про Redux с т.з. пользы и вреда. На счет того, что он дает удобство сопровождения понятно. А стор же отжирает память? Есть, скажем, таблица транзакций слева в 100 000 записей под гиг, которую хочет быстро проскролить аудитор, а справа форма с данными по выделенной транзакции. Если данные из выделенной строки перекидывать в форму не напрямую, а через стор, он же будет содержать дубликаты данных, которые уже находится в DOM-объекте HTMLTableElement.table.rows, да? Т.е. памяти страница отожрет в 2 раза больше как минимум. Еще наверняка есть какие-то обертки для каждой строки, которые слушают изменения в сторе, если данные транзакции поменялась из формы. Плюс журналируются изменные стэйты — еще минус память. Или все не так плохо с этой технологией, как кажется? Ну это так :)


        1. vvvvvvv
          23.12.2019 20:19

          Да, конечно будет кучу памяти отжирать. Но гигабайт данных отправлять в браузер одним куском, без пагинации? Тут у JS начнутся проблемы, не только у редакса. Это особенный случай, и решение понадобится тоже особое.


        1. bohdan-shulha
          23.12.2019 20:42
          +2

          > таблица транзакций слева в 100 000 записей под гиг, которую хочет быстро проскролить аудитор

          Если честно, не уверен, что это получится быстро даже на голом DOM. Обычно, когда работают с настолько большими датасетами, используют виртуализацию (react-virtualized, react-window).

          > Т.е. памяти страница отожрет в 2 раза больше как минимум

          Отожрёт если с прозрачностью ссылок что-то пойдёт не так, и только на отображаемую сущность.

          > Еще наверняка есть какие-то обертки для каждой строки, которые слушают изменения в сторе

          Нету. Стор модифицируется запуском редьюсера в ответ на action, который прокинули в систему (да, тут нюанс — запустятся все редьюсеры, если говорить о классических редьюсерах на switch/case). Дальше работает shallow comparison (проверяется ссылочная эквивалентность данных), чтобы избежать лишних ре-рендеров.

          > Плюс журналируются изменные стэйты

          Журналируются только action (обычный JS объект со строковым свойством type и вашей информацией), и только когда был подключён Redux Dev Tools или схожий middleware.

          UPD: redux, кстати, только хранит данные, а уж как их отображать — не его задача. Вы можете работать хоть с VanillaJS и использовать redux, главное — подписаться на стор и адекватно реагировать на его изменения.


    1. justboris
      23.12.2019 20:13

      гигантской инвалидной каляской

      интересно, чем обусловлено такое мнение?


  1. artalar
    23.12.2019 17:44

    Можно получить все тоже самое и даже больше(!) за 2КБ. Вот небольшой пруф


  1. kubk
    23.12.2019 22:20
    +1

    Ментейнеры Redux определённо проделали хорошую работу над ошибками. Всегда причислял к проблемам Редакса нечитабельное обновление вложенных объектов и сложности с типизацией. Наконец-то это дошло и до ментейнеров. Однако в Redux есть проблемы, не решаемые by design:

    1) Необходимость держать стейт в нормализованном виде из-за иммутабельности. Иммутабельное обновление данных подразумевает, что все вложенные объекты тоже должны быть скопированы, что будет триггерить перерисовку компонентов, данные в которых остались по факту теми же. Об этом написано в документации. Проблема решается нормализацией данных, что только добавляет головной боли — придётся нормализовывать данные с бекенда перед вставкой в стор и денормализовывать обратно перед отправкой на сервер. Получается ORM на фронте, с Mobx это не нужно.

    2) Для мемоизации нужно писать селекторы с ручным указанием зависимостей. Допустим у вас есть страница с товарами и кнопка «Load more», которая запрашивает товары с сервера пачками по N штук. Кнопка должна пропасть, если страниц с товарами больше нет. Это произойдёт когда количество загруженных товаров на странице станет равно общему количеству товаров на бекенде.

    Код на reselect будет выглядеть так:

    export const getProductPage = (state: RootState) => state.productPage;
    
    export const isLastPage = createSelector(
      createSelector(getProductPage, page => page.products),
      createSelector(getProductPage, page => page.totalCount),
      (products, totalCount) => products.length === totalCount
    );
    

    Код на Mobx будет выглядеть так:

    @computed get isLastPage() {
      return this.products.length === this.totalCount;
    }
    

    Не нравятся декораторы — используйте функции. Декораторы inject и observer больше не нужны с выходом хуков. Mobx будет автоматически пересчитывать значение isLastPage когда хотя бы одна из зависимостей (products или totalCount) изменится.

    Вычисления в селекторах на практике оказываются более сложными — например таблица с множественными аггрегациями по определённым полям и строкам. В таких случаях пересчитывания на каждый рендер компонента могут ухудшить UX. Для этих целей придумали мемоизацию и в Redux это делается очень многословно.


    1. mayorovp
      24.12.2019 06:51
      +1

      Декораторы inject и observer больше не нужны с выходом хуков.

      Страница, на которую вы дали ссылку, рассказывает только про inject, который в mobx-react изначально был пятым колесом в телеге. Декоратор observer всё ещё нужен.


      Хук useObserver лучше не использовать в качестве основного решения. Даже там, куда вы дали ссылку, про него написано так:


      Low level implementation used internally by observer HOC and Observer component.

      И ещё вот так:


      Despite using useObserver hook in the middle of the component, it will re-render a whole component on change of the observable. If you want to micro-manage renders, feel free to use <Observer />


      1. kubk
        24.12.2019 10:52
        -2

        Despite using useObserver hook in the middle of the component, it will re-render a whole component on change of the observable.

        Классовый компонент с декоратором observer будет работать так же — компонент будет полностью перерисовываться когда хотя бы одно из observable значений в render методе поменяется. Если хотим меньше перерисовок — либо разбиваем компонент на компоненты поменьше (тогда перерисовываться будут только дочерние), либо используем <Observer />. На практике мне всегда хватало первого варианта.

        Декоратор observer всё ещё нужен.

        Я не точно выразился. Имел в виду, что не обязательно прописывать experimentalDecorators в tsconfig, Mobx может использоваться и без декораторов:

        export const Counter = observer(() => {
          return (
            <div>
              <span>{counter.count}</span>
              <button onClick={counter.inc}>Increment</button>
            </div>
          )
        })
        

        Работает так же, как и старый пример с декоратором:

        @observer
        export class Counter {
          render() {
            <div>
              <span>{counter.count}</span>
              <button onClick={counter.inc}>Increment</button>
            </div>
          }  
        }
        

        Хуки же позволяют полностью переключиться с классовых компонентов на функциональные. Уже давно перестал использовать декораторы observer и inject, проблем в продакшене не было.


        1. mayorovp
          24.12.2019 11:03

          Классовый компонент с декоратором observer будет работать так же

          Да, но не будет иллюзии локальности, как и рисков накосячить с хуками.


    1. kolalexo Автор
      24.12.2019 17:57

      Если использовать хуки Redux получится лаконичнее:


      import { useSelector } from 'react-redux';
      
      const isLastPage = useSelector(
        ({ page }) => page.products.length === page.totalCount
      );


      1. kubk
        24.12.2019 20:43
        -1

        Идея в том, что селектор должен пересчитываться только тогда, когда поменяются переменные, на основе которых он считается — products и totalCount. В вашем примере это не соблюдается. Предлагаю более наглядный пример из документации Redux c зависимыми селекторами:

        const getVisibilityFilter = state => state.visibilityFilter;
        const getTodos = state => state.todos;
        
        export const getVisibleTodos = createSelector(
          [getVisibilityFilter, getTodos],
          (visibilityFilter, todos) => {
            switch (visibilityFilter) {
              case 'SHOW_ALL':
                return todos;
              case 'SHOW_COMPLETED':
                return todos.filter(t => t.completed);
              case 'SHOW_ACTIVE':
                return todos.filter(t => !t.completed);
            }
          }
        );
        
        const getKeyword = state => state.keyword;
        
        const getVisibleTodosFilteredByKeyword = createSelector(
          [getVisibleTodos, getKeyword],
          (visibleTodos, keyword) =>
            visibleTodos.filter(todo => todo.text.includes(keyword))
        );
        

        На Mobx это будет выглядеть так:

        @computed get visibleTodos() {
          switch (this.visibilityFilter) {
            case 'SHOW_ALL':
              return this.todos;
            case 'SHOW_COMPLETED':
              return this.todos.filter(t => t.completed);
            case 'SHOW_ACTIVE':
              return this.todos.filter(t => !t.completed);
          }
        }
        
        @computed get visibleTodosFilteredByKeyword() {
          return this.visibleTodos.filter(todo => todo.text.includes(this.keyword));
        }
        

        Мало того, что пример на Mobx объективно меньше и проще, так он ещё удобнее для рефакторинга — зависимости селектора не нужно прописывать вручную, а значит если нужно добавить ещё одну переменную на основе которой вычисляется значение, то мы добавим её в одном месте, а не в двух.


        1. kubk
          24.12.2019 22:13

          Господа минусующие, потрудитесь объяснить в чём я не прав. Ставить минусы исподтишка много ума не надо.


          1. romanovAA
            25.12.2019 09:51
            +1

            По-моему Ваши коментарии не связаны с темой статьи. За это вы и получаете минусы.


            1. mayorovp
              25.12.2019 09:54

              Да всё проще же. Просто кто-то с комментарием несогласен. Такое бывает с любыми комментариями. Но это не повод обвинять других пользователей в глупости.


              1. romanovAA
                25.12.2019 10:02

                Зачем приводить пример из mobx если статья рассказывает про инструмент для redux? Я вообще не увидел в статье использования селекторов. Есть упоминание что toolkit содержит реселект, но каждый может подключить любую другую библиотеку для создания селекторов, которая больше нравится.


                Не вижу смысла в коментариях выше и в своих тоже. :)


                1. mayorovp
                  25.12.2019 10:06

                  Чтобы было с чем сравнить, разве нет?


                  1. romanovAA
                    25.12.2019 10:08

                    Если бы статья называлась "Сравниваем mobx и redux", то да.


                    1. mayorovp
                      25.12.2019 10:08

                      Если бы статья называлась "Сравниваем mobx и redux", то комментарий как раз был бы не нужен, сравнение было бы уже в статье.


                      1. romanovAA
                        25.12.2019 10:19

                        То есть в статье автор рассматривает инструмент, который помогает разработчику redux, а комментатор приводит пример кода на mobx в котором без всяких инструментов можно получить хороший результат. Логично стремление автора коментария показать, что он умеет лучше. Но не логично что он делает это под статьей про инструмент, а не библиотеку полностью или же что-то похожее. За это он получил минус, это мое мнение, ваше мнение, что он получил минус потому что кто-то не согласен с ним, что вполне может быть. Предлагаю на этом закончить. С Новым Годом!


  1. Dron007
    24.12.2019 07:09
    -1

    Как-то всё очень разрозненно в этих конструкторах на Redux, даже официальных. Те же селекторы болтаются отдельно, аналога React Dev Tools для них нет, хотя по сути это варианты представления Redux Store, семантически относятся к нему, так же как и мутаторы — что синхронные, что асинхронные. Популярность этой библиотеки крайне низкая, как для официальной — 2200 звёзд в Git. Можно сравнить с альтернативными, более комплексными решениями: Mobx, easy-peasy, overmind (на который перешёл codesandbox).

    В react/redux стеке очень не хватает единообразия. Буквально на каждом уровне (компоненты/общий стор) по-своему организовывается кэширование и отслеживание изменений, а также хранение состояния и вычислимых свойств на его основе. Часто с помощью инородных сторонних библиотек типа reselect. Аналогично и для изменений — синхронные мутации встроены, асинхронные — через сторонние библиотеки вроде Thunk или Saga. Столько всего нагородили, а способа увидеть состояние приложения в целом — нет.


    1. kolalexo Автор
      24.12.2019 12:28

      Redux DevTools способен обрабатывать модифицированный селекторами store, и выводить его в общем виде. А селекторы “болтаются отдельно”, потому что не являются часто используемой фичей.
      Такое понятие “мутаторы” к Redux не относится.
      Низкая популярность прежде всего связана с тем, что релиз библиотеки версии 1.0 состоялся всего 2 месяца назад.
      Redux toolkit как раз и призван решить проблемы выбора подходящих средств из всего разнообразия в типичных случаях использования.


      1. Dron007
        24.12.2019 14:41

        Простите, как же селекторы не являются часто используемой фичей? Что вообще можно использовать вместо них, чтобы не повторять себя при сложных выборках из стора и чтобы мемоизировать выборки? Даже в самом этом Redux Toolkit встроена библиотека reselect. И селекторы не модифицируют стор вообще-то, идут отдельно от него, хотя логичнее, чтобы всё было в комплекте, как с геттерами в Vuex.


        1. kolalexo Автор
          24.12.2019 15:22

          В большинстве случаев необходимо использовать прямую выборку из стэйта. Предпочтительное использование мемоизации заключается не в получении сложных выборок, а кэшировании комплексных, часто запрашиваемых данных в случае, если начинает провисать производительность. По поводу производительности и ее преждевременной оптимизации можно ознакомиться со следующей статьей: optimization guide.


          Стор хранит нормализованные данные, данные можно нормализовывать как угодно до их попадания в стор. А после пользоваться ими как удобно. Селекторы модифицируют изъятые данные в конкретном месте, а Redux DevTools может это отображать как цельное состояние, это и имелось ввиду.


          1. Dron007
            24.12.2019 16:13

            Это понятно, что мемоизация и сложные выборки — разные вещи, я же через «и» перечислил.

            Стор хранит нормализованные данные, допустим, авторов и их книги с связями. Допустим, нам в разных компонентах надо считать число книг по авторам или выводить число книг, помеченных как новинка. Логично же сделать селектор, чтобы не повторять себя в каждом компоненте. Логично желание видеть значение этого селектора для отладки безотносительно того, в каком компоненте мы его используем. Это же свойство стора, одно из его расширенных представлений. Как view и процедуры в базе данных. Редьюсеры — как хранимые процедуры, меняющие стор. На данный момент не нашёл нормальных инструментов для решения этой задачи. Есть полузаброшенный проект для reselect, не позволяющий работать с большим числом селекторов, не выводящий их названия.


            1. kolalexo Автор
              24.12.2019 17:53

              Логично желание видеть значение этого селектора для отладки безотносительно того, в каком компоненте мы его используем.

              Последние версии Redux Devtools (2.15 точно) это позволяют.


              1. Dron007
                24.12.2019 18:23

                Вы уверены? На данный момент селекторы — чужеродный для Redux элемент. Всё что я нашёл — вот это, где обсуждается необходимость данной фичи. Даже не представляю себе как бы Redux Devtools мог выводить информацию о селекторах без серьёзных изменений в способе их создания.


                1. kolalexo Автор
                  24.12.2019 18:53

                  Привожу пример:
                  Селектор -


                  import { createSelector } from 'reselect';
                  
                  const getNodes = (state) => state.nodes.data;
                  
                  const treeNodesSelector = createSelector(
                    getNodes,
                    (nodes) => { ... Строится дерево из массива (добавляется поле children, к элементам)}
                  );
                  
                  export default treeNodesSelector;

                  MapStateToProps —


                  nodes: treeNodesSelector(state),

                  Хранится в store в виде массива — data: [].


                  Результат в DevTools —


                    nodes: {
                      data: [
                        {
                          id: '1',
                          ip: '127.0.0.1',
                          name: 'What is Love?',
                          port: 1,
                          parent_id: null,
                          children: [
                            {
                              id: '6',
                              ip: '11.1.1.1',
                              name: '11',
                              port: 1,
                              parent_id: '1'
                            }
                          ]
                        },...


                  1. Dron007
                    24.12.2019 19:57

                    Всё правильно, но это React Dev Tools (я там в одном сообщении опечатался, имея в виду Redux Dev Tools). Это уже результат работы селекторов и просто свойства, переданные в конкретные React компоненты. В Redux Dev Tools только Redux state. Было бы удобнее видеть общую картину — состояние Redux state и всех зависимых от него селекторов, а не выискивать компоненты, где эти селекторы используются (они могут быть глубоко в дереве, генерироваться по условиям и т.д.), попутно бегая в код, чтобы смотреть какие селекторы в какие свойства мапятся.


          1. Dron007
            24.12.2019 16:29

            Кроме того, селекторы позволяют абстрагироваться от внутренней организации стора, упрощая последующий рефакторинг. Тут неплохо описано. Поэтому любые решения, где они идут отдельно от стора и без возможности их удобного мониторинга, выглядят несколько куцыми.


            1. kolalexo Автор
              24.12.2019 17:53

              Из статьи по вашей ссылке: you are not required to use selector functions in a Redux app, а также сказано “Similarly, you don't have to use the Reselect library to create selectors — you can just write plain functions if you want”


              1. Dron007
                24.12.2019 18:33

                Не совсем понимаю. Разве это противоречит тому, что я написал выше? Разумеется, для простейших приложений, которые не будут развиваться и не потребуют рефакторинга, можно завязаться жёстко на структуру стора. Но разве это можно назвать хорошим стилем? Да и далее в статье «you are encouraged to use selector functions, and to use the Reselect library for memoized selectors» и сама вся статья об этом: «we recommend using selectors to encapsulate the knowledge of where a given piece of state lives. Ideally, only your reducer functions and selectors should know the exact state structure, so if you change where some state lives, you would only need to update those two pieces of logic.»


                1. kolalexo Автор
                  24.12.2019 18:34

                  Для построения абстраций не обязательно использовать дополнительные библиотеки.


  1. Druu
    24.12.2019 09:15
    +1

    Ох блин, наконец дошло до людей что action+reducer+action creator это просто функция. Пройдет еще годик и дойдет, что можно дропнуть все эти велосипеды и писать старое доброе


    export class ProductReleases extends Store {
      productReleasesFetching(state) {
        state.fetchingState = 'requesting';
      }
    
      productReleasesFetched(state, action) {
        state.productReleases = action.payload.productReleases;
        state.fetchingState = 'success';
      }
    
      productReleasesFetchingError(state, action) {
        state.fetchingState = 'failed';
        state.error = action.payload.error;
      }
      …
     };

    с ровно тем же результатом


  1. DopeboyYela
    25.12.2019 15:35
    +1

    Доброго времени суток.

    Подскажите пожалуйста касательно Redux Toolkit.
    Мое приложение выводит список постов из группы vk.com. Сами данные получаю без пагинации, данных 30к+. Данные храню в редаксе в виде массива. Без использования toolkit при фетчинге наблюдались небольшие лаги, так как я в целом не делал никакой оптимизации.
    Но после внедрения toolkit по данной статье начались сильные лаги при манипуляции со стором. В чем может быть причина и как это исправить?

    Спасибо :)


    1. kolalexo Автор
      25.12.2019 16:17

      Вероятнее всего лагает из-за выполнения проверок на иммутабельность хранилища и сериализуемость объектов хранилища. Отключить эти проверки можно в параметрах функции getDefaultMiddleware.


      getDefaultMiddleware({
        immutableCheck: false,
        serializableCheck: false,
        thunk: true,
      });