React + Redux:

Data flow management

Andrey Sinitsyn

goo.gl/v4g9hc

Основные проблемы

Общее

  • Ужасная организация кода

React

  • Отсутствие явных ролей у компонентов
  • prop-hell

Redux

  • Повторяющиеся редьюсеры, которые можно упростить

Что такое prop-hell?..

...примерно это

<MessageComponent
  message={this.state.currentMessage}
  shouldComponentGetHighlighted={this.state.isInFocus}
  {...filteredMessageProps}
  onClick={this.boundMessageClickHandler}
  messageType={this.getMessageType(this.state.currentMessage)}
  messageMeta={this.getMessageMeta(this.state.currentMessage)}
  messageActions={this.props.messageActions}
  isDeleted={confirmDeletion(this.state.currentMessage)}
  userSelector={this.props.userSelector}
  dialogDataSelector={this.props.dialogDataSelector}
  contactsMetaSelector={this.props.contactsMetaSelector}
  routingSelector={this.props.routingSelector}
 />

Что с этим делать?

npm i -S reprovide

Что это такое?

  • Способ организации кода...
  • ...подход к проектированию приложения...
  • ...а ещё...

...Dependency Injection.
В Реакте.
Прощай, миллард пропов, передаваемых компоненту!

Организация кода, проектирование

  • Display-компоненты: отрисовка
  • Провайдеры: предоставление данных

Два типа компонентов:

Что такое Display?

  • Обычный React.Component, React.PureComponent, whatever...
  • ...который не имеет доступа к данным напрямую.

Что такое Provider?

  • Компонент, реализующий класс persephone.ProviderComponent
  • Имеет метод getProvider(), который возвращает объект - данные, которые провайдер предоставляет
  • (опционально) реализует метод shouldComponentReprovide(), благодаря которому провайдер может выполнять повторную мемоизацию
import { 
  Provider,
  ProviderComponent
} from 'reprovide';

@Provider('RandomValueGeneratorProvider')
class RandomValueGeneratorProvider
 extends ProviderComponent {
    shouldComponentReprovide(nextProps, nextState) {
        return true;
    }

    getProvider() {
        return {
            value: () => Math.random()
        }
    }
}

Так, а как он будет предоставлять value?

  • Метод render() ProviderComponent автоматически дописывает текущий Provider компонента в props.providers к своим детям и передаёт дальше
import React from 'react';
import { Inject } from 'reprovide';
import RandomNumberGeneratorProvider
    from '../RandomNumberGeneratorProvider';

@Inject(
  RandomNumberGeneratorProvider
)
class RandomNumberDisplay extends React.Component {
  render() {
    const { 
      providers: {
         RandomNumberGeneratorProvider 
      }
    } = this.props;

    return (
      <div>RNG got us: {RandomNumberGeneratorProvider.value()}</div>
    );
  }
}

Что такое Inject?

Inject - HOC, позволяющий оборачивать компонент в Провайдеры, которые мы ему передали.
Записи далее эквивалентны

@Inject(SomeProvider)
class RandomComponent extends React.Component {
    render() {
        const { 
            providers: { SomeProvider } 
        } = this.props;
        return SomeProvider.someValue;
    }
}

// later
<RandomComponent />
class RandomComponent extends React.Component {
    render() {
        const { 
            providers: { SomeProvider } 
        } = this.props;
        return SomeProvider.someValue;
    }
}

// later
<SomeProvider>
    <RandomComponent />
</SomeProvider>

Плюсы подхода

  • Зависимости явно выражены в компоненте
  • Отладка стала проще
  • Слои представления и данных чётко разграничены

Reprovide.
Prop-hell в прошлом!

npm i -S reprovide

Поговорим о Redux

Классический редьюсер

switch(action.type) {
    case 'UPDATE_MESSAGES_SUCCESS':
        return updateStateUpdate(state, action.payload);
    case 'FETCH_MESSAGES_SUCCESS':
        return updateStateFetch(state, action.payload);
    default:
        return state;
}

Проблемы?

Хватает!

  • Не скейлится нейминг экшнов
  • Boilerplate. Таких редьюсеров у вас может быть 10, 20 и больше

 

  • switch - убог

Решения?

  • Приведём нейминг к единому формату, уменьшающему шанс возникновения коллизий
    (а заодно подчистим код)

 

  • Попробуем пошагово генерализировать редьюсер, чтобы этот и последующие редьюсеры можно было создавать парой строчек кода

Нейминг и организация кода?

Уточка спешит на помощь!

Ducks.
Что это и почему это классно

  • Ducks - паттерн организации кода, согласно которому все сущности Redux должны быть изолированы
  • Для каждой сущности создаётся один файл - entity.js
  • Экспорт по умолчанию - редьюсер
  • Экшны и прочее экспортируется как обычно
  • Экшны нужно называть как:
    appName/duckName/action

Почему Ducks это НЕ классно

  • Вы всерьёз хотите файлы в 500+ строк, описывающие каждую сущность?

Ducks, который не sucks

  • Одна сущность - одна папка, entity
  • entity/actions/ - экшны
  • entity/reducer.js - редьюсер, экспортируется из entity/index.js по дефолту
  • Экшны называются как
    appName/duckName/entityName/status
  • Остальное - на усмотрение
     

Было

Стало

...а теперь редьюсер.

npm i -S reducio

А это что ещё такое?

  • Набор хелперов, позволяющий писать редьюсеры в функциональном стиле.
  • Две функции - createReducer и composeReducers

createReducer

Принимает

  • Предикат, принимающий action и возвращающий Boolean. Если возвращает true - редьюсер выполняется
  • Функцию, принимающую state и action и возвращающую новый state
  • Функцию, принимающую action и возвращающую набор полей, которые будут добавлены к action с помощью Object.assign

Возвращает

  • Редьюсер - функцию (state, action) => {/* code */}

composeReducers

​Принимает:

  • vararg - редьюсеры, которые нужно склеить в один

Возвращает

  • Функцию
const x = composeReducers(reducer1, reducer2, reducer3)

// внутреннее устройство x
function x(state, action) {
    return reducer1(
            reducer2(
                reducer3(state, action),
                action
            ),
            action
        );
}

Упрощаем: утилиты

const createActionEqualityPredicate = 
  value =>
    action =>
      action.type === value

const updateStateKey = 
  key =>
    (state, action) =>
      immutablyUpdate(state, 
        { [key]: { $set: action.payload } })

// immutablyUpdate - ваша утилита
// для иммутабельного обновления данных

Упрощаем: CREATE reducer

// Ещё одна утилита, условно доступная глобально
const genActionType = 
    (duck, entity, action, status) =>
        `app/${duck}/${entity}/${action}/${status}`

const commonEntityUpdater = 
    (state, action) => 
      updateStateKey(`list.${action.payload._id}`)
        (state, action)

const createCreationReducer = (duck, entity) => (
  createReducer(
    createActionTypeEqualityPredicate(
        genActionType(duck, entity, 'create', 'success')
    ),
    commonEntityUpdater
  )
)

Упрощаем: RETRIEVE reducer

const createRetrievalReducer = (duck, entity) => (
  createReducer(
    createActionTypeEqualityPredicate(
        genActionType(duck, entity, 'retrieve', 'success')
    ),
    updateStateKey('list')
  )
)

Упрощаем: UPDATE reducer

const createUpdateReducer =  (duck, entity) => (
  createReducer(
    createActionTypeEqualityPredicate(
        genActionType(duck, entity, 'update', 'success')
    ),
    commonEntityUpdater
  )
)

Упрощаем: DELETE reducer

const createDeleteReducer = (duck, entity) => (
  createReducer(
    createActionTypeEqualityPredicate(
        genActionType(duck, entity, 'delete', 'success')
    ),
    (state, action) =>
      updateImmutably(
        state,
        { 
          [`list.${action.payload._id}`]: 
          { $delete: true }
        }
      )
  )
)

Всё вместе?

const createEntityReducer = (duck, entity) => (
  composeReducers(
   createCreationReducer(duck, entity),
   createRetrievalReducer(duck, entity),
   createUpdatingReducer(duck, entity),
   createDeletingReducer(duck, entity)
  )
)

const messagesReducer = createEntityReducer(
  'messages',
  'message'
)
1. npm i -S reducio
2. ???
3. PROFIT!

Вопросы?

telegram: @asn007

github: @asn007

React+redux: data flow management

By Andrey Sinitsyn (asn007)

React+redux: data flow management

  • 1,032