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