Архитектура модульных React + Redux приложений

  • Tutorial


Большинство разработчиков начинает знакомство с Redux с Todo List Project. Это приложение имеет следующую структуру:

actions/
  todos.js
components/
  todos/
    TodoItem.js
    ...
constants/
  actionTypes.js
reducers/
  todos.js
index.js
rootReducer.js

На первый взгляд такая организация кода кажется логичной, ведь она напоминает стандартные соглашения многих backend MVC-фреймворков:

app/
  controllers/
  models/
  views/

На самом деле, это неудачный выбор как для MVC, так и для React+Redux приложений по следующим причинам:

  1. С ростом приложения следить за взаимосвязью между компонентами, экшнами и редюсерами становится крайне сложно
  2. При изменении экшна или компонента с большой вероятностью потребуется внести изменения и в редюсер. Если количество файлов велико, скролить IDE вверх/вниз не удобно
  3. Такая структура потворствует копипасте в редюсерах

Не удивительно, что многие авторы(раз, два, три) советуют структурировать приложение по «функциональности» (by feature).

Мы достаточно давно пришли к такому-же выводу в бекэнд-разработке., поэтому во фронтэнде поступаем также. В русском языке нет подходящего перевода для слова feature как единицы функциональности. Вместо него мы употребляем слово «модуль». В ES6 термин «модуль» имеет другое значение. Чтобы не путать их между собой в случае неоднозначности можно использовать словосочетание «модуль приложения». В повседневной работе сложностей не возникало, кроме этого термин «модуль» хорошо понятен и подходит для коммуникации с бизнес-пользователями.

Модульная структура

Мо́дуль — функционально законченный фрагмент программы.

Мо́дульное программи́рование — это организация программы как совокупности небольших независимых блоков, называемых модулями, структура и поведение которых подчиняются определённым правилам.

Модульное приложение в моем понимании должно отвечать следующим требованиям:

  1. Весь код модуля располагается в одной папке. Чтобы полностью удалить модуль из программы достаточно удалить соответствующую папку. Удаление модуля не нарушает работоспособности других модулей, но лишает приложение части функциональности.
  2. Модули не зависимы друг от друга. Модификация любого модуля не влияет на работу других модулей. Допускается зависимость модулей от «ядра» системы.
  3. Ядро системы содержит публичное API, предоставляющее модулям средства ввода/вывода и набор компонентов для создания UI.

Получаем такую структуру приложения:

app/
  modules/
     Module1/
         …
         index.js
     Module2/
         …
         index.js
     …
     index.js
  core/
      …
  index.js
  routes.js
  store.js

В точку входа помещаем AppContainer, необходимый для react-hot-reload, со вложенным компонентом Root. Root содержит только Provider, обеспечивающий связь с redux и react-router, определяющий точку входа в приложение с помощью indexRoute. Компонент можно вынести в npm-пакет и подключать в любом приложении, т.к. он только инициализирует инфраструктуру и не содержит логики предметной модели.

index.js


import 'isomorphic-fetch'
import './styles/app.sass'

import React from 'react'
import ReactDOM from 'react-dom'

import { AppContainer } from 'react-hot-loader'
import browserHistory from './core/history'
import Root from './core/containers/Root'
import store from './store';
import routes from './routes';


  ReactDOM.render(
    <AppContainer>
      <Root store={store}
            history={browserHistory}
            routes={routes}/>
    </AppContainer>,
    document.getElementById('root'));

Root.js



import React from 'react'
import PropTypes from 'prop-types'
import {Provider} from "react-redux"
import {Router} from "react-router"

const Root = props => (
<Provider store={props.store}>
  <Router history={props.history} routes={props.routes} />
</Provider>)

Root.propTypes = {
  history: PropTypes.object.isRequired,
  routes: PropTypes.array.isRequired,
  store: PropTypes.object.isRequired
}

export default Root

Пока все достаточно просто. Нам осталось подключить модульную систему к состоянию (store) и настроить роутинг.

defineModule


Напишем небольшую функцию:

export const defineModule = (
  title,
  path,
  component,
  reducer = (state = {}) => state,
  onEnter = null) => {
    return {title, path, component, reducer, onEnter}
}

Создадим в папке modules модуль личного кабинета пользователя.

modules/
     Profile/
         Profile.js
         index.js

Profile/Profile.js


import React from 'react'
import PropTypes from 'prop-types'
const Profile = props => (<h2>Привет, {props.name}</h2>)
Profile.propTypes = {
  name: PropTypes.string.isRequired
}

export default Profile

Profile/index.js



const SET_NAME = 'Profile/SetName'
const reducer (state = {name: ‘Василий’}, action) => {
   switch(action.type){
        case SET_NAME: {…state, name: action.name}
   }
}
export default defineModule('Личный кабинет', '/profile, Profile')

И зарегистрируем модуль в файле modules/index.js

import Profile from './Profile'

export default {
  Profile
}

Этого шага можно избежать, но для наглядности, оставим ручную инициализацию модульной структуры. Две строчки импорта/экспорта написать не так сложно.

Я использую CamelCase и / для лучшей читаемости в названиях экшнов. Для того, чтобы было проще собирать, можно воспользоваться такой функцией:

export const combineName = (...parts) => parts
  .filter(x => x && toLowerCamelCase(x) != DATA)
  .map(x => toUpperCamelCase(x))
  .reduce((c,n) => c ? c + '/' + n : n)

const Module = 'Profile'
const SET_NAME = combineName(Module, 'SetName')

Осталось подключить личный кабинет к роутеру и вставить модуль в лейаут. С лейаутом все просто. Создаем core/components/App.js. Обратите внимание, что в компонент Navigation передается тот же массив, что и в роутер, чтобы избежать дублирования.

import React from 'react'
import PropTypes from 'prop-types'
import Navigation from './Navigation'

const App = props => (
  <div>
    <h1>{props.title}</h1>
    <Navigation routes={props.routes}/>
    {props.children}
  </div>)

App.propTypes = {
  title: PropTypes.string.isRequired,
  routes: PropTypes.array.isRequired
}

export default App

Роутер


А с роутером будет немного сложнее. В общем случае должна быть возможность ассоциировать с модулем более одного URL. Например /profile содержит основную информацию о профиле, а /profile/transactions – список транзакций пользователя. Допустим Мы хотим всегда выводить имя пользователя в личном кабинете, а ниже вывести компонент с двумя табами: «общая информация» и «транзакции».

Тогда, логичная структура роутов будет такой:

  <Router>
      <Route path="/profile" component={Profile}>
         <Route path="/info" component={Info}/>
          <Route path="/transactions" component={Transaction}/>
      </ Route >
  </Router>

Компонент Profile будет выводить имя пользователя и табы, а Info и Transactions – детали профиля и список транзакций соответственно. Но необходимо также поддерживать вариант, когда компоненты модуля не нуждаются в дополнительном группирующем модуле (например, список заказ и окно просмотра заказа являются независимыми страницами).

Введем соглашение


Из модуля можно экспортировать объект структурой как возвращаемый из функции defineModule или массив таких объектов. Все компоненты будут добавлены в список роутов без дополнительной вложенности.

Модуль может содержать ключ children, содержащий структуру, аналогичную файлу modules/index.js. В этом случае один из них должен называться Index. Он будет использован в качестве IndexRoute. Тогда мы получим структуру, соответствующую «личному кабинету».

Воспользуемся моноидальной природой списка и получим плоский массив модулей с учетом возможности экспортировать массив или объект.

export const flatModules = modules => Object.keys(modules)
  .map(x => {
    const res = Array.isArray(modules[x]) ? modules[x] : [modules[x]]
    res.forEach(y => y[MODULE] = x)
    return res
  })
  .reduce((c,n) => c.concat(n))

В Router можно передавать не только компоненты Route, но и просто массив с обычными объектами, чем мы и воспользуемся.

export const getRoutes = (modules, store, App, Home, title = 'Главная') =>
  [
    {
      path: '/',
      title: title,
      component: App,
      indexRoute: {
        component: Home
      },

      childRoutes: flatModules(modules)
        .map(x => {
          if (!x.component) {
            throw new Error('Component for module ' + x + ' is not defined')
          }

          const route = {
            path: x.path,
            title: x.title,
            component: x.component,
            onEnter: x.onEnter
              ? routeParams => {
                x.onEnter(routeParams, store.dispatch)
              }
              : null
          }

          if(x.children){
            if(!x.children.Index || !typeof(x.children.Index.component)){
              throw new Error('Component for index route of "' + x.title + '" is not defined')
            }

            route.indexRoute = {
              component: x.children.Index.component
            }

            route.childRoutes = Object.keys(x.children).map(y => {
              const cm = x.children[y]
              if (!cm.component) {
                throw new Error('Component for module ' + x + '/' + y + ' is not defined')
              }

              return {
                path: x.path + cm.path,
                title: cm.title,
                component: cm.component,
                onEnter: cm.onEnter
                  ? routeParams => {
                    cm.onEnter(routeParams, store.dispatch)
                  }
                  : null
              }
            })
          }

          return route
        })
    }
  ]

Таким образом добавление модуля в файл modules/index.js будет автоматически инициализировать новые роуты. Если разработчик забудет объявить роут или запутается в соглашениях, то увидит в консоли недвусмысленное сообщение об ошибке.

onEnter


Обратите внимание на то, что модуль также может экспортировать функцию onEnter. В которую при переходе на соответствующий роут, будут переданы параметры пути и функция store.dispatch. Это позволяет избежать использования componentDidMount для инициализации компонентов. Вместо этого можно выкинуть в store событие (или Promise, если вы, как я, решили выкинуть redux-saga и оставить redux-thunk), обработать его в редюсере, модифицировать state, вызвав тем самым перерисовку компонента.

Подключаем редюсеры к стору
Нам понадобятся DevTools и thunk. Объявим небольшую функцию для инициализации стора.

const composeEnhancers = typeof window === 'object'
  && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
  ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
  : compose;

const createAppStore = (reducer, ...middleware) => {
  middleware.push(thunk)
  const store = createStore(
    reducer,
    composeEnhancers(applyMiddleware(...middleware)))
  return store
}

export default createAppStore

И еще одну для получения и компоновки всех редюсеров для всех модулей:


export const combineModuleReducers = modules => {
  const reducers = {}
  const flat = flatModules(modules)
  for (let i = 0; i < flat.length; i++) {
    const red = flat[i].reducer
    if (typeof(red) !== 'function') {
      throw new Error('Module ' + i + ' does not define reducer!')
    }

    reducers[flat[i][MODULE]] = red

    if(typeof(flat[i].children) === 'object'){
      for(let j in flat[i].children){
        if(typeof(flat[i].children[j].reducer) !== 'function'){
          throw new Error('Module ' + j + ' does not define reducer!')
        }

        reducers[j] = flat[i].children[j].reducer
      }
    }
  }

  return reducers
}

Можно сделать менее строго и просто пропускать модули, не содержащие редюсеров, а не падать с исключением, но мне по душе более строгий подход. Если модуль не содержит вообще никакой логики, проще оформить его просто компонентом и добавить в роутер вручную.

Совмещаем все в файле store.js


export default createAppStore(combineReducers(combineModuleReducers(modules)))


Теперь каждому модулю соответствует часть стейта, совпадающая с ключем в файле modules/index.js. Для личного кабинета это будет Profile
На этом про структуру модульных приложений у меня все. Организация «ядра» и предоставление публичного API модулям – тема отдельной статьи.
Метки:
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 41
  • +3
    И что, реально получается сделать модули независимыми друг от друга? Как быть с модулями а-ля дашборды, агрегирующими данные из многих источников? Как быть с модулями объективно связанных коллекций сущностей типа клиентов и заказов, когда нужно то конкретного клиента посмотреть со всеми его заказами, то список заказов с ФИО клиента?
    • 0

      Выглядит очень увлекательно. А чего демку куда-нибудь не выложили, чтобы можно было руками пощупать что получилось?

      • 0
        Согласен, хочется посмотреть результат.
        • 0
          Возможно, выложу в open-source в среднесрочной перспективе. Пока нет достаточно времени для того, чтобы аккуратно все оформить. Вообще, приложенного кода достаточно, чтобы самостоятельно развернуть такую структуру, хотя придется конечно потратить время.
      • 0
        Модули не зависимы друг от друга. Модификация любого модуля не влияет на работу других модулей. Допускается зависимость модулей от «ядра» системы.
        Это неизбежно приводит к распуханию ядра от всякой никак не связанной с ним фигни. Или вы смогли как-то это решить?
        • 0
          Сложно точно ответить без знания специфики вашего проекта. Расскажу как у нас. Любой подход, технология — работают в рамках каких-то ограничений. У нас большой объем разработки «учетных систем» и прочей «автоматизации бизнеса». Энтерпрайз короче. Самые часто используемые компоненты — это гриды, пагинации, деревья, формы. Т.е. инструменты манипуляции с данными. Все эти компоненты конфигурируются схемой данных, чтобы избежать неявного дублирования кода.

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

          Если нужно сделать 2 примерно одинаковых модуля, но один — с перламутровыми пуговицами, используется техника copy/paste. Да, если захочется изменить оба модуля, то придется в два раза больше исправлять, но эта а) эту работу можно дать двум разработчикам, а не одному, б) если модули были чуть-чуть разными, то при модификации с большой вероятностью они разъедутся со всеми вытекающими из «хрупкого базового класса».

          Чтобы ядро не «распухало» для UI нужно выбирать какие-то пакеты компонентов и ставить через npm.
          • +1

            А в чём проблема повторного использования графиков и прочего rich ui?

            • 0
              Ну я больше имел в виду ситуации, когда, есть у вас например модуль с дэшбордом и модуль с виджетом. Дэшборд настраивается пользователем, можно выбрать разные типы виджетов, лежащих, соответственно, в разных модулях (ну, если мы по фичам делим). Как избежать связывания?
              • 0
                А что мешает положить эту логику в App и использовать как мастер-пейдж для всего приложения? Или там гипотетические 100500 дешбордов в 27 конфигурациях?
                • 0
                  Ну т.е. появляется некий «App», который «где-то» лежит, но если все на модулях, то модуль App ссылается на остальных. Упс.
                  Ну и опять же, с какой стати App должен знать о связи Dashboard -> Widget?
                  Это просто палка о двух концах, все хорошо делится на модули до появления первого связующего модуля, а потом начинаются пляски вокруг Core/Shared/<как_угодно>.

                  Edit: ну, dashboard — один из многих разделов большого App, сунуть его туда целиком нельзя.
                  • 0
                    Я думаю, это уже очень специфичный для проекта вопрос. Может быть в вашем случае монолит — более удачное решение.
                    • 0

                      Связанность между модулями — это нормально в целом. Ненормальна высокая связанность, особенно с нарушением инкапсуляции используемого модуля.

                      • 0
                        И как отличить, когда связанность высокая, а когда не очень, особенно если над проектом работает человек 50 из трех часовых поясов?)
                        • 0

                          Субъективно, конечно. Обычно понимаешь, что связь высокая когда в рамках одной задачи приходится перелопачивать кучу кода в разных модулях.

          • +2
            если вы, как я, решили выкинуть redux-saga и оставить redux-thunk

            Можно прокомментировать эту фразу? Очень любопытно, почему Вы отказались от Саги.
            • 0
              1. Полифилы для генераторов чего-то весят, сага чего-то весит, бандл получается увесистый
              2. Не все понимают генераторы. Крутая кривая обучения
              3. Компоновка саг — не тривиально

              Т.е. больше из-за ограничений практического применения. С теоретической точки зрения мне саги больше нравятся.
            • +1
              const Profile = default (props) => (<h2>Привет, {props.name}</h2>)

              Никогда раньше такого не видел, зачем тут default?

              • +1
                Ошибка при форматировании. Исправил.
              • +1

                Про onEnter, получите и распишитесь. :)


                v4 is a complete rewrite. As such, there is no singular breaking change. We have some similar-looking things (<Route path="/foo" component={Foo} />), but the behaviors are completely different. You should expect none of your existing react-router usage to work under 4.0.

                As for the on* hooks, they were removed because they already exist in React as the lifecycle methods. We were reimplementing them inside the Router, but they didn't exactly behave like they should. So, why provide an inferior or conflicting API? Everything is way more React-y now, and it's way better as a result. You no longer have to mix paradigms or code for two different systems. It's just React now. It's a much lower cognitive load.

                And for "plain routes", we've extracted that to a separate package: https://github.com/ReactTraining/react-router/tree/master/packages/react-router-config You can see the reasoning for this here:

                The approach being taken for 4.0 is to strip out all the "batteries included" kind of features and get back to just basic routing. If you need query string parsing or async loading or Redux integration or something else very specific, then you can add that in with a library specifically for your use case. Less cruft is packed in that you don't need and you can customize things to your specific preferences and needs.
                • 0
                  Пока четвертый роутер вообще использовать нельзя серьезно. С переходом можно легко «поправить» эту проблему. Так как все компоненты регистрируются в роутере в одном файле — routes.js, то просто оборачиваем целевой компонент в обертку с вызовом onEnter. Рефакторинга на 30 минут.
                  • –1
                    На этой неделе на хабре цикл статей инфы 3х летней давности?
                    • +3

                      В мире JS опять всё поменялось? :-)

                  • 0
                    Такой же подход применяется в Drupal CMS
                    • 0
                      При вашем подходе проектом легче управлять, есть ТЗ на модуль, есть ответственный.
                      Что внутри не так и важно, лишь бы работал.
                      Мне лично более симпатичен подход, когда в команде есть несколько спецов, которые пишут инструмент, а остальные его применяют. Но здесь требуется более тесное взаимодействие.
                      Это не критика, наверняка у вас все продумано. Так, рассуждения.

                      Есть вопрос по поводу redux: какой смысл использовать mapStateToProps? Умный контейнер прекрасно передает глупым компонентам свое состояние и каллбэки, setState вызывает цепочку рендеров. Реализация через редюсеры — это еще один уровень сложности. Зачем?
                      Я понимаю, когда нужно отправить команду на самый верх, чтобы она сверху вниз взбодрила кого надо, но в отношении родитель/дети redux лишний (имхо). Простое лучше сложного.
                      Спасибо.
                      • 0
                        Все верно, не надо складывать в стор абсолютно весь стейт приложения. Только необходимые app-wide вещи. Все остальное отлично умещается в контейнере.
                        • 0
                          Мне лично более симпатичен подход, когда в команде есть несколько спецов, которые пишут инструмент, а остальные его применяют. Но здесь требуется более тесное взаимодействие.

                          У нас «инструмент» — это ядро. Ядро пишут несколько человек. Остальная команда в основном использует. По примеру специалист еще не знакомый со стеком, может реализовать простой модуль не вникая в то, что под капотом. Более сложные модули требуют более высокой квалификации.

                          Есть вопрос по поводу redux: какой смысл использовать mapStateToProps? Умный контейнер прекрасно передает глупым компонентам свое состояние и каллбэки, setState вызывает цепочку рендеров. Реализация через редюсеры — это еще один уровень сложности. Зачем?
                          Я понимаю, когда нужно отправить команду на самый верх, чтобы она сверху вниз взбодрила кого надо, но в отношении родитель/дети redux лишний (имхо).

                          Эту часть не вполне понял :) Может привести пример?
                          • +1

                            Вообще не использую локальный стейт. Всё в сторе redux-а. Минус один уровень абстракции. Простое лучше сложного. :)


                            Есть такая болезнь — коннектобоязнь, описана Ильей Климовым в докладе.

                            • +2
                              Пока, скажем так, адекватного решения проблемы нет

                              Чудесно. Для сравнения:


                              1. Открываем: http://mol.js.org/#demo=mol_app
                              2. Открываем консоль и включаем эмуляцию GPRS на вкладке Network для наглядности.
                              3. Вручную меняем локаль на английскую: $mol_locale.lang( 'en' )
                              4. Во всех блоках, где выводились локализованные строки включился индикатор загрузки.
                              5. Через какое-то время все русские тексты поменялись на английские.

                              И для этого не пришлось вообще ничего специально делать. Достаточно просто получить текст таким образом: $mol_locale.text( [ '$mol_app_demo' ] , 'title' )

                              • 0

                                Я почти уверен, что mol прекрасен. Больше того, может озолотить (когда-то меня приняли без экзаменов в Мамбу, как опытного спеца по велосипеду OnPHP). Но пока буду дальше жевать кактус по имени React. :)

                                • 0
                                  vintage ну вот блин, я тоже смотрел на $mol в свободное время. Споткнулся о несусветную документацию из серии «ну вот я так думаю, а дальше вы сами». И пост я ваш читал о введении в ваш движок. И обсуждение абсолютно в каждом топике на хабре, хоть как-то связанном с фронтендом, вы так или иначе сводите к своему движку. Но, елки палки, сделайте нормальные selling туториалы с описанием как решать стандартные наболевшие проблемы. Роутинг, сайд эффекты, композиция компонентов, переиспользование компонентов, вы же понимаете о чем я.

                                  Вердикт: вместо того, чтобы распыляться тут в комментариях, пишите больше статей в ключе практического применения своего решения.
                                  • 0
                                    Но, елки палки, сделайте нормальные selling туториалы с описанием как решать стандартные наболевшие проблемы.

                                    У меня всего 2 руки и нет единомышленников. Туториалы я пишу по мере возможности. Но думаю дело пошло бы быстрее, если бы вы озвучили с какими конкретно трудностями столкнулись, чтобы я раскрыл эту тему подробнее.


                                    Роутинг: https://github.com/eigenmethod/mol/tree/master/state/arg
                                    сайд эффекты — тут что имеется ввиду?
                                    композиция компонентов, переиспользование компонентов: https://github.com/eigenmethod/mol/tree/master/view

                                  • +2

                                    Ну а пока вы ужинаете, немного аналитики...


                                    Итак, выводим семьсот с мелочью круглячков в форме треугольника серпинского, каждый из которых раз в секунду меняет содержимое, а всё целиком ресайзится каждый фрейм.


                                    React


                                    image


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


                                    React Fiber


                                    image


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


                                    $mol


                                    image


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


                                    Я даже пробовал прикрутить "квантификацию", причём не только рендеринга, но и вообще всех вычислений, чтобы анимации не подтупливали, когда рендерится что-то огромное, но это приводило к общему замедлению рендеринга (квантификация не бесплатна) и глюкам типа как у react fiber, когда возюкаешь мышью и часть страницы замирает. В результате вернулся всё же на вариант с полным обновлением за раз.

                                    • 0
                                      Может быть графика и вывод UI — это разные задачи? В gamedev'е отдельный поток на рендер — вроде как стандарт. Мне кажется, что рисование 100500 треугольников с помощью вращающихся кружков — вообще не кейс использования реакта
                              • 0
                                Спасибо за ссылку, хотя 50 неоднородных компонент на странице — это редкий случай.
                                Пример: есть панель со списком вложений. Рядом с именем файла крестик (удалить). Передать компонентам нужно свойство readOnly, контейнерную функцию (clickOnKrestik) и что-то статичное. Зачем здесь redux?
                                Я не против сторе, я против того, чтобы делать ВСЕ на редюсерах.
                                Задача: нужно сделать 6 панелей с однотипными компонентами и наваять из них около 20 форм для работы с БД. Связь родитель/ребенок, все просто и понятно, никаких mapDispatchToProps и mapStateToProps.
                                SPA вовсе не означает, что все должно быть на одной странице в одном контейнере.
                                • +1

                                  Повсеместно применяю функциональные (stateless) компоненты. Изобрел вот такой велосипед:


                                  export const pureComponent = (fn) => {
                                    class Wrapper extends React.PureComponent {
                                      render() {
                                        return fn(this.props, this.context)
                                      }
                                    }
                                    // не надо, т.к. подписывает на контекст как и функциональный компонент,
                                    // так и оболочку-PureComponent; лучше назначать сразу оболочке (снаружи)
                                    // Wrapper.contextTypes = fn.contextTypes
                                    Wrapper.displayName = fn.name
                                    return Wrapper
                                  }

                                  И теперь нужно следить только за props-ами.


                                  Ещё разглядывал сегодня redux-persist, например. Для PWA сгодится.

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