Чесний MVC на React + Redux

MVC
Ця стаття про те, як побудувати архітектуру web-додатки у відповідності з принципами MVC на основі React і Redux. Перш за все, вона буде цікава тим розробникам, хто вже знайомий з цими технологіями, або тим, кому належить використовувати їх у новому проекті.

Model-View-Controller
Концепція MVC дозволяє розділити дані (модель), подання та обробку дій (вироблену контролером) користувача на три окремі компоненти:
  1. Модель (англ. Model):
    • Надає знання: дані і методи роботи з цими даними;
    • Реагує на запити, змінюючи свій стан;
    • Не містить інформації, як ці знання можна візуалізувати;
  2. Подання (англ. View) — відповідає за відображення інформації (візуалізацію).
  3. Контролер (англ. Controller) — забезпечує зв'язок між користувачем і системою: контролює введення даних користувачем і використовує модель та подання для реалізації необхідної реакції.
React в ролі View
React.js це фреймворк для створення інтерфейсів від Facebook. Всі аспекти його використання ми розглядати не будемо, мова піде про Stateless-компоненти і React виключно в ролі View.
Розглянемо наступний приклад:
class FormAuthView extends React.Component {
componentWillMount() {
this.props.tryAutoFill();
}
render() {
return (
<div>
<input 
type = "text" 
value = {this.props.login}
onChange = {this.props.loginUpdate}
/>
<input 
type = "password" 
value = {this.props.password}
onChange = {this.props.passwordUpdate}
/>
<button onClick = {this.props.submit}>
Submit
</button>
</div>
); 
}
}

Тут ми бачимо оголошення 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. Такий підхід порушує односпрямованість потоку даних в додатку і підвищує зв'язність компонентів між собою. І те й інше — погано.
Що буде, якщо відступити від MVC
Також деякі Stateful-компоненти можуть мати проблеми з серверним рендеренгом, адже їх відображення визначається не тільки за допомогою Props.
А ще слід пам'ятати, що в Redux Action-и оборотні, але зміни State всередині компонентів — ні, і якщо змішати така поведінка — нічого хорошого не вийде.
Висновок
Сподіваюся, опис чесного MVC підходу при розробці з використанням React і Redux буде корисна розробникам для створення оптимальної архітектури свого web-додатки.
Якщо є можливість повною мірою використовувати концепцію MVC, то давайте її використовувати, і не потрібно винаходити щось інше. Це підхід перевірений на міцність десятиліттями, і всі його плюси, мінуси і підводні камені давно відомі. Придумати щось краще навряд чи вийде.
Кожен раз, коли ти змішуєш Логіку і Відображення, Бог вбиває кошеня
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.