В процессе разработки интерфейсов с использованием react-компонентов часто возникает соблазн написать логику изменения состояния в ответ на действие пользователя тут же в классе компонента в функции-обработчике пользовательского события. Хоть такой подход и кажется простым и понятным, для большого приложения это приводит к сложности поддержки и неразберихе.
В мире React придумали решать эту проблему с помощью архитектуры Flux. У нас для работы с данными и состоянием приложения используется одновременно библиотеки Redux (локальное состояние приложения) и Relay (данные из бекенда). Этот документ регламентирует правила, по которым мы описываем бизнес-логику приложения.
Компоненты, которые располагаются в папках components
(view-компоненты), должны решать только три задачи:
props
) в виде пользовательского интерфейса.За исключением очень специфических низкоуровневых задач, в общем случае компоненты не должны использовать своё
локальное состояние (свойство state
) и изменять его в обработчиках (setState
).
В общем случае, внешний вид компонента должен меняться только в ответ на изменение свойств (props
),
произошедшее извне, которое в свою очередь может быть произведено экшном, вызванным из обработчика пользовательского
события в компоненте.
Вызов экшна в компоненте должен быть очень простым и коротким и может иметь две формы:
props
) из компонента-контейнера (аннотация
@container
) или проброшен из родительского компонента. Этот способ предпочтительный.dispatch
(аннотация @connect
) с нужным экшн-креатором, импортированным из папки
actions
или через DI (аннотация @injectProps
). Этот способ следует применять только в случаях, когда
вызываемый экшн специфичен только для данного типа представления и не может быть описан в контейнере.Пример view-компонента:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | import SomeContainer, {ContainerProps} from 'common/containers/SomeContainer'
import * as someActions from '../actions/someActions'
interface ReduxProps {
showHint?: boolean
toggleSomeHint?: () => void
}
@container(SomeContainer)
@connect(mapStateToProps, someActions)
class SomeComponent extends React.Component<ContainerProps & ReduxProps, void> {
render() {
const {showHint, toggleSomeHint, somePropFromContainer} = this.props
return (
<div className={cx({hidden: !showHint})}
onClick={toggleSomeHint}>
Some hint
</div>
<div onClick={this.handleClick}>
Действие {somePropFromContainer}
</div>
)
}
handleClick = () => {
// это вырожденный случай, его можно было и напрямую в jsx прописать,
// но обычно тут надо что-нибудь из DOM достать
this.props.doTheRightThing()
}
}
function mapStateToProps(state: GlobalState) {
return {
showHint: state.some.showHint,
}
}
|
Компоненты, которые располагаются в папках containers
(компоненты-контейнеры), выполняют следующие задачи:
actions
), особенно если это многоходовочка с поэтапным изменением
состояния (например, типично: вызвали api, показали спиннер, дождались результата, показали ОК или ошибку).Примечание
Может возникнуть вопрос:
A зачем нужна эта лишняя прослойка callback-функций в контейнере, когда можно вызывать экшны непосредственно из view-компонентов?
Дело в том, что экшнам обычно требуются дополнительные параметры, которыми владеет контейнер, но которые не нужны логике представления (view-компоненту). Перекладывание вызова экшна на view-компонент может привести к перегрузке компонента-представления лишними свойствами и знаниями, не относящимися непосредственно к внешнему виду.
Зачастую, компонент-контейнер является общим для разных типов представлений (мобильное/web). К view-компоненту он
подключается с помощью аннотации @container
.
Пример контейнера для компонента из предыдущего раздела:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | import CommonActions from '../actions/CommonActions'
interface OwnProps {
viewer: any
}
export interface ContainerProps extends OwnProps {
somePropFromContainer: string
doTheRightThing: () => void
}
interface Props extends OwnProps {
view: React.ComponentClass<ContainerProps>
commonActions: CommonActions
}
@injectProps('commonActions')
@relayContainer({
viewer: () => Relay.QL`
fragment on Viewer {
{SomeCoolMutation.getFragment('viewer')},
}
`,
})
@connect(mapStateToProps)
class SomeContainer extends React.Component<Props, void> {
render() {
renderView(this.props, {
doTheRightThing: this.doTheRightThing,
})
}
private doTheRightThing = () => {
const {dispatch, commonActions, viewer} = this.props
// вызываем сложное действие, передавая параметры, которыми владеет контейнер
dispatch(commonActions.doTheRightThing(viewer))
}
}
function mapStateToProps(state: GlobalState) {
return {
somePropFromContainer: state.common.actionName,
}
}
|
Примечание
В этом документе название экшн (action) используется как более удобное сокращение от action-creator. Подразумевается одно и то же.
Action-creator’ы — это функции, которые возвращают либо непосредственно структурку redux-экшна, либо thunk-action —
функцию, которая внутри себя асинхронно диспатчит (dispatch) серию других redux-экшнов вперемешку с вызовами других
сервисов. Экшны оформляются либо в виде плоских es6-модулей, либо, если необходимо применить DI, в виде
классов-сервисов, расположенных в папке actions/
бандла.
Все асинхронные цепочки действий с промисами, многоэтапные действия и т.п., в том числе и вызовы Relay-мутаций, должны описываться в action-creator’ах.
Action-creator’ы могут быть как общими для всех типов представлений (тогда они используются в основном на уровне компонентов-контейнеров), так и специфическими для данного типа представления (например, только для web-приложения).
Пример сервиса с экшнами:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import {ThunkAction} from 'redux-thunk'
import {commitUpdate} from 'utils/relay'
export default class CommonActions {
doTheRightThing = (viewer: any): ThunkAction<void> => {
return (dispatch) => {
dispatch(startDoingRightThing())
commitUpdate(new SomeCoolMutation({viewer}))
.then(() => dispatch(rightThingDone())
.catch(err => dispatch(rightThingFailed(err.message))
}
}
}
|