Честный MVC на React + Redux
Эта статья о том, как построить архитектуру web-приложения в соответствии с принципами MVC на основе React и Redux. Прежде всего, она будет интересна тем разработчикам, кто уже знаком с этими технологиями, или тем, кому предстоит использовать их в новом проекте.
Model-View-Controller
Концепция MVC позволяет разделить данные (модель), представление и обработку действий (производимую контроллером) пользователя на три отдельных компонента:
Модель (англ. Model):
- Предоставляет знания: данные и методы работы с этими данными;
- Реагирует на запросы, изменяя своё состояние;
- Не содержит информации, как эти знания можно визуализировать;
Представление (англ. View) — отвечает за отображение информации (визуализацию).
- Контроллер (англ. Controller) — обеспечивает связь между пользователем и системой: контролирует ввод данных пользователем и использует модель и представление для реализации необходимой реакции.
React в роли View
React.js это фреймворк для создания интерфейсов от Facebook. Все аспекты его использования мы рассматривать не будем, речь пойдет про Stateless-компоненты и React исключительно в роли View.
Рассмотрим следующий пример:
class FormAuthView extends React.Component {
componentWillMount() {
this.props.tryAutoFill();
}
render() {
return (
);
}
}
Здесь мы видим объявление Functional-component-а FormAuthView. Он отображает форму с двумя Input-ами для логина и пароля, а также кнопку Submit.
FormAuthView это Stateless-компонент, т.е. он не имеет внутреннего состояния и все данные для отображения получает исключительно через Props. Также через Props этот компонент получает и Callback-и, которые эти данные меняют. Сам по себе этот компонент ничего не умеет, можно назвать его «Глупым», так как никакой логики обработки данных в нем нет, и сам он не знает, что за функции он использует для обработки пользовательских действий. При создании компонента он пытается использовать Callback из Props для автозаполнения формы. Про реализацию функции автозаполнения формы этому компоненту тоже ничего неизвестно.
Это пример реализации слоя View на React.
Redux в роли Model
Redux является предсказуемым контейнером состояния для JavaScript-приложений. Он позволяет создавать приложения, которые ведут себя одинаково в различных окружениях (клиент, сервер и нативные приложения), а также просто тестируются.
Использование Redux подразумевает существование одного единственного объекта Store, в State которого будет хранится состояние всего вашего приложения, каждого его компонента.
Чтобы создать Store, в Redux есть функция createStore.
createStore(reducer, [preloadedState], [enhancer])
Её единственный обязательный параметр это Reducer. Reducer это такая функция, которая принимает State и Action, и в соответствии с типом Action определенным образом модифицирует иммутабельный State, возвращая его измененную копию. Это единственное место в нашем приложении, где может меняться State.
Определимся какие Action-ы нужны, для работы нашего примера:
const EAction = {
FORM_AUTH_LOGIN_UPDATE : "FORM_AUTH_LOGIN_UPDATE",
FORM_AUTH_PASSWORD_UPDATE : "FORM_AUTH_PASSWORD_UPDATE",
FORM_AUTH_RESET : "FORM_AUTH_RESET",
FORM_AUTH_AUTOFILL : "FORM_AUTH_AUTOFILL"
};
Напишем соответствующий Reducer:
function reducer(state = {
login : "",
password : ""
}, action) {
switch(action.type) {
case EAction.FORM_AUTH_LOGIN_UPDATE:
return {
...state,
login : action.login
};
case EAction.FORM_AUTH_PASSWORD_UPDATE:
return {
...state,
password : action.password
};
case EAction.FORM_AUTH_RESET:
return {
...state,
login : "",
password : ""
};
case EAction.FORM_AUTH_AUTOFILL:
return {
...state,
login : action.login,
password : action.password
};
default:
return state;
}
}
И ActionCreator-ы:
function loginUpdate(event) {
return {
type : EAction.FORM_AUTH_LOGIN_UPDATE,
login : event.target.value
};
}
function passwordUpdate(event) {
return {
type : EAction.FORM_AUTH_PASSWORD_UPDATE,
password : event.target.value
};
}
function reset() {
return {
type : EAction.FORM_AUTH_RESET
};
}
function tryAutoFill() {
if(cookies && (cookies.login !== undefined) && (cookies.password !== undefined)) {
return {
type : EAction.FORM_AUTH_AUTOFILL,
login : cookies.login,
password : cookies.password
};
} else {
return {};
}
}
function submit() {
return function(dispatch, getState) {
const state = getState();
dispatch(reset());
request('/auth/', {send: {
login : state.login,
password : state.password
}}).then(function() {
router.push('/');
}).catch(function() {
window.alert("Auth failed")
});
}
}
Таким образом, данные приложения и методы работы с ними описаны с помощью Reducer и ActionCreators. Это пример реализации слоя Model с помощью Redux.
React-redux в роли Controller
Все React-компоненты так или иначе будут получать свой State и Callback-и для его изменения только через Props. При этом ни один React-компонент не будет знать о существовании Redux и Actions вообще, и ни один Reducer или ActionCreator не будет знать о React-компонентах. Данные и логика их обработки полностью отделены от их представления. Я хочу особенно обратить на это внимание. Никаких «Умных» компонентов не будет.
Напишем Controller для нашего приложения:
const FormAuthController = connect(
state => ({
login : state.login,
password : state.password
}),
dispatch => bindActionCreators({
loginUpdate,
passwordUpdate,
reset,
tryAutoFill,
submit
}, dispatch)
)(FormAuthView)
На этом всё: React-компонент FormAuthView получит login, password и Callback-и для их изменения через Props.
Результат работы этого демо-приложения можно посмотреть на Codepen.
Что нам дает такой подход
- Использование только Stateless-компонентов. Большую часть которых можно написать в виде Functional-component, что является рекомендованным подходом, т.к. они быстрее всего работают и потребляют меньше всего памяти
- React-компоненты можно переиспользовать с разными контроллерами или без них
- Легко писать тесты, ведь логика и отображение не связаны между собой
- Можно реализовать Undo/Redo и использовать Time Travel из Redux-DevTools
- Не нужно использовать Refs
- Жесткие правила при разработке делают код React-компонентов однообразным
- Отсутствуют проблемы с серверным рендерингом
Что будет, если отступить от MVC
Велик соблазн сделать какие-то компоненты поудобнее и написать их код побыстрее, завести внутри компонента State. Мол какие-то его данные временные, и хранить их не нужно. И всё это будет работать до поры до времени, пока, например, вам не придется реализовать логику с переходом на другой URL и возвращением обратно — тут всё сломается, временные данные окажутся не временными, и придется всё переписывать.
При использовании Stateful-компонентов, чтобы достать их State, придется использовать Refs. Такой подход нарушает однонаправленность потока данных в приложении и повышает связность компонентов между собой. И то и другое — плохо.
Также некоторые Stateful-компоненты могут иметь проблемы с серверным рендеренгом, ведь их отображение определяется не только с помощью Props.
А еще следует помнить, что в Redux Action-ы обратимы, но изменения State внутри компонентов — нет, и если смешать такое поведение — ничего хорошего не получится.
Заключение
Надеюсь, описание честного MVC подхода при разработке с использованием React и Redux будет полезно разработчикам для создания правильной архитектуры своего web-приложения.
Если есть возможность в полной мере использовать концепцию MVC, то давайте её использовать, и не нужно изобретать что-то другое. Это подход проверенный на прочность десятилетиями, и все его плюсы, минусы и подводные камни давно известны. Придумать что-то лучше навряд ли получиться.
Комментарии (5)
18 июля 2016 в 07:21
–1↑
↓
Ну зачем же представлять Господа, как злого и мстительного тимлида, навязывающего всем свое мнение?)))18 июля 2016 в 07:56
0↑
↓
Ну или даже вот такой вопрос — зачем писать используя Flux и называть это MVC? По сути у вас все view обращаются к одной модели через контроллеры этих вьюх посредством кидания экшнов, а модель автоматически обновляет вьюхи… эм, ну то есть у вас однонаправленное движение данных, как и нужно в Flux.18 июля 2016 в 07:56
0↑
↓
простите, не туда откомментировал :)
18 июля 2016 в 07:49
0↑
↓
А зачем строить MVC на редуксе с реактом когда их связка создана ради Flux-архитектуры? А если нет возможности использовать Flux то зачем использовать редукс когда можно использовать что-либо более MVC с реактом?18 июля 2016 в 09:02
0↑
↓
Мне кажется, что я использую связку React+Redux вполне стандартным образом. Просто я ввел некоторые ограничения, которые гарантируют, что все плюсы, которые дает нам Redux не будут сведены на нет. Про возможное убийство плюсов я писал.
Насчет «если нет возможности использовать Flux то зачем использовать редукс» не могу согласиться. Redux — всё-таки это не Flux (про отличия от предшественников можно почитать тут https://github.com/reactjs/redux/blob/master/docs/introduction/PriorArt.md)
Зачем строить MVC? Потому что плюсов масса, и потому что это легко сделать — посмотрите код всего приложения, там никаких кастомных решений нет — всё беру из коробки.