React против Angular 2: почему мы выбрали React для CRM-системы
Осенью 2016 года мы начинали CRM-проект: ~60 экранов, сложные формы, обновления в реальном времени, дашборд с несколькими потоками данных. Перед стартом потратили две недели на сравнение React (с Redux) и Angular 2 (RC5 на тот момент).
Оба варианта были жизнеспособны. Вот что повлияло на выбор.
Angular 2: комплектность из коробки
Angular 2 - это полноценный фреймворк. Не нужно выбирать библиотеки: маршрутизация, HTTP-клиент, формы, внедрение зависимостей, i18n - всё включено.
Двустороннее связывание через [(ngModel)] поначалу выглядит магически просто:
@Component({
template: `
<input [(ngModel)]="contact.name" />
<span>{{ contact.name }}</span>
`
})
class ContactFormComponent {
contact = { name: '' };
}
Изменил contact.name в любом месте - input и span обновились автоматически. Красиво для простых форм.
TypeScript с самого начала - Angular 2 был дальновидным в 2016 году, когда TS только начинал набирать популярность.
Почему двустороннее связывание опасно в сложных системах
Магия становится проблемой, когда приложение растёт. С двусторонним связыванием данные могут измениться откуда угодно в любой момент.
На предыдущем Angular 1 проекте мы потратили несколько часов на поиск бага: форма с 40 полями, одно из которых сбрасывалось при определённых действиях. Причина - несколько $watch в разных компонентах мутировали общее состояние, и отследить причинно-следственную связь было мучительно.
Главная идея React: данные текут в одну сторону. Родитель передаёт данные вниз через props. Дочерний компонент отправляет события вверх. Состояние живёт в предсказуемом месте.
Redux: предсказуемое состояние для CRM
В CRM данные взаимосвязаны: контакт связан со сделкой, сделка - с задачами, задачи - с уведомлениями. Глобальное состояние неизбежно.
Redux даёт единый стор и строгий протокол изменений:
// Любое изменение состояния - через явный action
const contactsReducer = (state = initialState, action) => {
switch (action.type) {
case 'UPDATE_CONTACT':
return {
...state,
contacts: state.contacts.map(c =>
c.id === action.payload.id
? { ...c, ...action.payload.data }
: c
)
};
case 'ADD_DEAL_TO_CONTACT':
return {
...state,
contacts: state.contacts.map(c =>
c.id === action.payload.contactId
? { ...c, deals: [...c.deals, action.payload.deal] }
: c
)
};
default:
return state;
}
};
Ключевой инструмент - Redux DevTools. Расширение для браузера, которое записывает каждый action и позволяет перемотать историю состояния назад:
Воспроизвели баг → открыли DevTools → увидели точно какой action привёл к неправильному состоянию → исправили. С двусторонним связыванием такая трассировка невозможна в принципе.
Экосистема имела значение
В сентябре 2016 года экосистема React была богаче для наших задач:
- react-select - лучший multi-select компонент, для Angular 2 сопоставимого не было
- react-virtualized - рендер 10 000 строк таблицы без тормозов (у нас были большие списки контактов)
- react-dnd - drag-and-drop для канбан-доски сделок
- Recharts - графики, нативно интегрированные с React-рендерингом
Экосистема Angular 2 активно развивалась, но в 2016-м ещё не догнала.
Что мы выбрали и почему
React + Redux + React Router. Не потому что Angular 2 плохой - нет. А потому что:
- Однонаправленный поток данных соответствовал сложности проекта
- Redux DevTools сэкономили бы нам сотни часов отладки
- Нужные библиотеки уже были в экосистеме React
- Команда имела больше опыта с React
Angular 2 вышел финально в сентябре 2016. К 2018 году он стал отличным выбором. К 2020-му - первоклассным решением для крупных enterprise-проектов с большими командами.
Ретроспектива 2024
Сегодня вопрос «React vs Angular» для большинства проектов решается в пользу React/Next.js по умолчанию. Angular занял нишу крупных корпоративных систем - банки, госструктуры, большие команды, где важна жёсткая структура и TypeScript first-class.
Для нового CRM в 2024 году мы бы выбрали Next.js App Router с React Server Components. Принцип однонаправленного потока данных, который в 2016-м казался ограничением, сегодня встроен в саму архитектуру фреймворка.