Хабрахабр

Mrr: тотальное FRP для Реакта

Mrr — функционально-реактивная библиотека для React'а (извиняюсь за мнимую тавтологию).

Однако серия последних статей на эту тему на Хабре([1], [2], [3]) показала громоздкость решений на Rx, которые на несложных примерах проигрывали в ясности и простоте почти любому другому подходу. При слове «реактивность» обычно вспоминают Rx.js, как эталонный образец FRP. Но стали бы вы писать, к примеру, простую синхронную валидацию формы на Rx? Rx велик и могуч, и прекрасно подходит для решения проблем, в которых абстракция потока напрашивается сама собой (на практике это преимущественно координация асинхронных задач). Сэкономил бы он ваше время, по сравнению с обычными императивными подходами?

mrr — это попытка доказать, что ФРП может быть удобным и эффективным решением не только в специфических «потоковых» проблемах, но и в самых обычных рутинных задачах фронтенда.
Реактивное программирование — очень мощная абстракция, на данный момент на фронтенде она присутствует в двух ипостасях:

  • реактивные переменные (computed variables): просто, надежно, интуитивно понятно, но потенциал РП раскрыт далеко не полностью
  • библиотеки для работы с потоками, такие как Rx, Bacon и т.д.: мощно, но достаточно сложно, сфера практического использования ограничена специфическими задачами.

Mrr совмещает плюсы этих подходов. В отличии от Rх.js, mrr имеет краткий API, который пользователь может расширять своими дополнениями. Вместо десятков методов и операторов — четыре базовых оператора, вместо Observable (горячих и холодных), Subject и т.д. — одна абстракция: поток. Также в mrr отсутствуют некоторые сложные концепции, которые могут существенно усложнить читабельность кода, например, метапотоки.

Отталкиваясь от тех же базовых принципов, что и Rx, mrr претендует на бóльшую нишу: управление глобальным и локальным (на уровне компонента) состоянием приложения. Однако mrr — это не «упрощенный Rx на новый лад». Это и есть принцип «тотального ФРП». Хотя изначально концепция реактивного программирования предназначалась для работы с асинхронными задачами, mrr с успехом использует подходы реактивности и для обычных, синхронных задач.

Вместо такого «салата» из разных подходов и технологий внутри одного приложения, с mrr вы можете использовать единую технологию и парадигму. Часто при создании приложения на Реакте используется несколько разнородных технологий: recompose (или в скором времени — хуки) для состояния компонента, Redux/mobx для глобального состояния, Rx посредством redux-observable (или thunk/saga) для управления сайд-эффектами и координации асинхронных задач в Редаксе.

Благодаря абстракции реактивности и декларативному подходу, mrr позволяет писать выразительный и краткий код. Интерфейс mrr также существенно отличается от Rx и подобных библиотек — он более декларативен. К примеру, стандартное TodoMVC на mrr занимает менее 50 строк кода(не считая JSX шаблона).

Получилось ли совместить преимущества «легкого» и «тяжелого» RP в одном флаконе — судить вам, но сначала прошу ознакомиться с примерами кода. Но довольно рекламы.

Мы будем рассматривать mrr на примере условного приложения для покупки ж/д билетов. TodoMVC уже изрядно набил оскомину, а пример с загрузкой данных о пользователях Github слишком примитивен, чтобы на нем можно было прочувствовать особенности библиотеки. Затем, после отправки данных, будет возвращен список доступных поездов и мест в них. В нашем UI будут поля для выбора начальной и конечной станций, даты. Поехали. Выбрав конкретный поезд и тип вагона, пользователь введет данные пассажиров, и затем добавит билеты в корзину.

Нам нужна форма с выбором станций и даты:

Создадим поля с автодополнением для ввода станций.

import from 'mrr'; const stations = [ 'Абакан', 'Алматы', 'Альметьевск', 'Белая Церковь', ...
] const Tickets = withMrr({ // начальные значения потоков $init: { stationFromOptions: [], stationFromInput: '', }, // вычисляемый поток - "ячейка" stationFromOptions: [str => stations.filter(s => s.indexOf(str)===0), 'stationFromInput'],
}, (state, props, $) => { return (<div> <h3> Поиск жд билетов </h3> <div> Станция отправления: <input onChange={ $('stationFromInput') } /> </div> <ul className="stationFromOptions"> { state.stationFromOptions.map(s => <li>{ s }</li>) } </ul> </div>);
}); export default Tickets;

mrr-компоненты создаются с помощью функции withMrr, которая принимает схему реактивных связей (описание потоков) и render-функцию. Render-функции передаются props компонента, а также state, которым теперь полностью управляет mrr. В нем и будут находится начальные(блок $init) и вычисляемые по формулам значения реактивных ячеек.

Сейчас у нас есть две ячейки (либо два потока, что то же самое): stationFromInput, значения в которую попадают из пользовательского ввода с помощью хелпера $ (передающего по умолчанию event.target.value для элементов ввода данных), и производная от нее ячейка stationFromOptions, содержащая массив подходящих по названию станций.

Синтаксис выражений mrr прост: на первом месте идет функция (либо оператор), по которой высчитывается значение ячейки, затем идет список ячеек, от которых данная ячейка зависит: их значения передаются в функцию. Значение stationFromOptions автоматически вычисляется каждый раз при изменении родительской ячейки с помощью функции (в терминологии mrr называемой "формула" — по аналогии с формулами Экселя). Пока что логика mrr тут напоминает обычный подход с computable variables, используемый в Vue, Svelte и других библиотеках, с той лишь разницей, что вы можете использовать чистые функции. Такой странноватый, на первый взгляд, синтаксис имеет много преимуществ, которые мы позже рассмотрим.

Также необходимо скрывать список станций после того как юзер кликнет по одной из них. Реализуем подстановку выбранной из списка станции в поле ввода.

const Tickets = withMrr({ $init: { stationFromOptions: [], stationFromInput: '', }, stationFromOptions: [str => stations.filter(s => str.indexOf(a) === 0), 'stationFromInput'], stationFrom: ['merge', 'stationFromInput', 'selectStationFrom'], optionsShown: ['toggle', 'stationFromInput', 'selectStationFrom'],
}, (state, props, $) => { return (<div> <div> Станция отправления: <input onChange={ $('stationFromInput') } value={ state.stationFrom }/> </div> { state.optionsShown && <ul className="stationFromOptions"> { state.stationFromOptions.map(s => <li onClick={ $('selectStationFrom', s) }>{ s }</li>) } </ul> } </div>);
});

Обработчик событий, созданный с помощью хелпера $, в списке станций будет эмитировать фиксированные для кажной опции значения.

Мы не можем после выбора станции «принудительно» изменить значение ячейки. mrr последователен в своем декларативном подходе, чуждом любым мутациям. Вместо этого мы создаем новую ячейку stationFrom, которая, с помощью оператора объединения потоков merge (приблизительный аналог на Rx — combineLatest), будет собирать значения двух потоков: пользовательского ввода (stationFromInput) и выбора станции (selectStationFrom).

За видимость списка опций будет отвечать ячейка optionsShown, которая будет принимать булевы значения в зависимости от изменения других ячеек. Мы должны показывать список опций после того, как пользователь что-то вводит, и скрывать после того, как он выбрал одну из опций. Он устанавливает значение ячейки в true при любом изменении первого аргумента (потока), и в false — второго. Это очень распостраненный паттерн, для которого существует синтаксический сахар — оператор toggle.

Добавим кнопку для очистки введенного текста.

const Tickets = withMrr({ $init: { stationFromOptions: [], stationFromInput: '', }, stationFromOptions: [str => stations.filter(s => str.indexOf(a) === 0), 'stationFromInput'], clearVal: [a => '', 'clear'], stationFrom: ['merge', 'stationFromInput', 'selectStationFrom', 'clearVal'], optionsShown: ['toggle', 'stationFromInput', 'selectStationFrom'],
}, (state, props, $) => { return (<div> <div> Станция отправления: <input onChange={ $('stationFromInput') } value={ state.stationFrom }/> { state.stationFrom && <button onClick={ $('clear') }>Х</button> } </div> { state.optionsShown && <ul className="stationFromOptions"> { state.stationFromOptions.map(s => <li onClick={ $('selectStationFrom', s) }>{ s }</li>) } </ul> } </div>);
});

Теперь наша ячейка stationFrom, отвечающая за содержимое текста в поле ввода, собирает свои значения не с двух, а с трех потоков. Этот код можно упростить. Конструкция mrr вида [*формула*, *… ячейки-аргументы*] аналогична S-выражениям в Lisp'е, и как и в Лиспе, вы можете произвольно вкладывать такие конструкции друг в друга.

Давайте избавимся от малополезной ячейки clearVal и сократим код:

stationFrom: ['merge', 'stationFromInput', 'selectStationFrom', [a => '', 'clear']],

Программы, написанные в императивном стиле, можна сравнить с плохо организованным коллективом, где все постоянно что-то приказывают друг другу (намекаю на вызовы методов и мутирующие изменения), при чем как руководители подчиненным, так и наоборот. Декларативные же программы похожи на противоположную утопическую картину: коллектив, где каждый четко знает, как он должен действовать в любой ситуации. В таком коллективе нет нужды в приказах, все просто находятся на своих местах и работают, реагируя на происходящее.

какие значение примет ячейка при тех или иных изменениях других ячеек. Вместо того, чтобы описывать всевозможные последствия какого-то события (читай — делать те или иные мутации), мы описываем все случаи, при которых данное событие может наступить, т.е. Для привыкшего к императивному коду программиста такой подход может показаться непривычным (или даже «костылем», «извращением»). В нашем небольшом пока примере мы описали ячейку stationFrom и три ситуации, которые влияют на ее значение. На самом деле он позволяет экономить усилия за счет краткости (и стабильности) кода, в чем мы убедимся на практике.

Можно ли подтягивать список предложенных станций ajax'ом? А что насчет асинхронности? В сущности, для mrr все равно, вернет ли функция значение или промис. Без проблем! При возврате промиса mrr дождется его resolv'а и «протолкнет» полученные данные в поток.

stationFromOptions: [str => fetch('/get_stations?str=' + str).then(res => res.toJSON()), 'stationFromInput'],

Это также означает, что вы можете использовать асинхронные функции в качестве формул. Более сложные случаи (обработка ошибок, статус промиса) мы рассмотрим позже.

Дублировать то же самое для станции прибытия нет смысла, стоит вынести это в отдельный компонент, который можно повторно использовать. Функционал для выбора станции отправления готов. Это будет обобщенный компонент инпута с автодополнением, поэтому мы переименуем поля и сделаем, чтобы функция получения подходящих вариантов задавалась в props'ах.

const OptionsInput = withMrr(props => ({ $init: { options: [], }, val: ['merge', 'valInput', 'selectOption', [a => '', 'clear']], options: [props.getOptions, 'val'], optionsShown: ['toggle', 'valInput', 'selectOption'],
}), (state, props, $) => <div> <div> <input onChange={ $('valInput') } value={ state.val } /> </div> { state.optionsShown && <ul className="options"> { state.options.map(s => <li onClick={ $('selectOption', s) }>{ s }</li>) } </ul> } { state.val && <div className="clear" onClick={ $('clear') }> X </div> }
</div>)

Как видите, вы можете задать структуру ячеек mrr как функцию от props компонента (однако выполнится она лишь один раз — при инициализации, и не будет реагировать на изменение props).

Обмен данными между компонентами

Теперь подключим этот компонент в родительском компоненте и посмотрим, как mrr позволяет родственным компонентам обмениваться данными.

const getMatchedStations = str => fetch('/get_stations?str=' + str).then(res => res.toJSON()); const Tickets = withMrr({ stationTo: 'selectStationFrom/val', stationFrom: 'selectStationTo/val',
}, (state, props, $, connectAs) => { return (<div> <OptionsInput { ...connectAs('selectStationFrom') } getOptions={ getMatchedStations } /> - <OptionsInput { ...connectAs('selectStationTo') } getOptions={ getMatchedStations } /> <input type="date" onChange={ $('date') } /> <button onClick={ $('searchTrains') }>Поиск</button> </div>);
});

Чтобы связать родительский компонент с дочерним, мы должны передать ему параметры с помощью функции connectAs (четвёртый аргумент render-функции). При этом мы указываем имя, которое хотим дать дочернему компоненту. Присоединив таким образом компонент, по этому имени мы можем обращаться к его ячейкам. В данном случае, мы слушаем ячейки val. Возможно и обратное — слушать из дочернего компонента ячейки родительского.

При этом, опять же вследствии декларативности, угрозы вмешательства в работу другого компонента нет — мы не имеем возможности ничего в нем «изменить», мутировать, можно лишь «слушать» данные. Как видите, и здесь mrr следует декларативному подходу: не нужно никаких onChange колбэков, нам достаточно указать имя для дочернего компонента в функции connectAs, после чего мы получаем доступ к его ячейкам!

Сигналы и значения

Следующий этап — поиск подходящих поездов по выбранным параметрам. В императивном подходе мы бы наверняка написали некий обрабочик на отправку формы onSubmit, который бы инициировал дальнейшие действия — ajax запрос и отображение полученных результатов. Но, как вы помните, нам нельзя ничего «приказывать»! Мы можем только создать еще набор ячеек, производных от ячеек формы. Напишем еще один запрос.

const getTrains = (from, to, date) => fetch('/get_trains?from=' + from + '&to=' + to + '&date=' + date).then(res => res.toJSON()); const Tickets = withMrr({ stationFrom: 'selectStationFrom/val', stationTo: 'selectStationTo/val', results: [getTrains, 'stationFrom', 'stationTo', 'date', 'searchTrains'],
}, (state, props, $, connectAs) => { return (<div> <OptionsInput { ...connectAs('selectStationFrom') } getOptions={ getMatchedStations } /> - <OptionsInput { ...connectAs('selectStationTo') } getOptions={ getMatchedStations } /> <input type="date" onChange={ $('date') } /> <button onClick={ $('searchTrains') }>Поиск</button> </div>);
});

Однако, такой код не будет работать как ожидалось. Калькуляция (пересчет значения ячейки) запускается при изменении любого из аргументов, поэтому запрос будет отправляться, например, сразу после выбора первой станции, а не только по клику на «Поиск». Нам нужно, с одной стороны, чтобы станции и дата передавались в аргументы формулы, но с другой стороны, не реагировать на их изменение. В mrr для этого существует элегантный механизм, называемый «пассивное слушание».

results: [getTrains, '-stationFrom', '-stationTo', '-date', 'searchTrains'],

Просто добавляем минус перед именем ячейки, и вуаля! Теперь results будет реагировать только на изменение ячейки searchTrains.

— как «ячейки-значения». В этом случае ячейка searchTrains выступает как «ячейка-сигнал», а ячейки stationFrom и др. Для ячейки-значения важным есть именно ее значение, но при этом моменты ее изменения несущественны. Для ячейки-сигнала существенным является только момент, когда по ней «протекает» значние, при этом, какие именно данные это будут — все равно: это может быть просто true, «1» или что угодно (в нашем случае это будут объекты DOM Event). На уровне синтаксиса в mrr эти два вида ячеек никак не различаются, но само концептуальное понимание такого различия очень важно при написании реактивного кода. Эти два типа ячеек не взаимоисключающи: многи ячейки являются и сигналами, и значениями.

Расщепление потоков

Запрос на поиск мест в поезде может занять некоторое время, поэтому мы должны показывать лоадер, а также реагировать в случае ошибки. Для этого дефолтного подхода с автоматическим резолвингом промисов уже мало.

const Tickets = withMrr({ $init: { results: {}, } stationFrom: 'selectStationFrom/val', stationTo: 'selectStationTo/val', searchQuery: [(from, to, date) => ({ from, to, date }), '-stationFrom', '-stationTo', '-date', 'searchTrains'], results: ['nested', (cb, query) => { cb({ loading: true, error: null, data: null }); getTrains(query.from, query.to, query.date) .then(res => cb('data', res)) .catch(err => cb('error', err)) .finally(() => cb('loading', false)) }, 'searchQuery'], availableTrains: 'results.data',
}, (state, props, $, connectAs) => { return (<div> <div> <OptionsInput { ...connectAs('selectStationFrom') } getOptions={ getMatchedStations } /> - <OptionsInput { ...connectAs('selectStationTo') } getOptions={ getMatchedStations } /> <input type="date" onChange={ $('date') } /> <button onClick={ $('searchTrains') }>Поиск</button> </div> <div> { state.results.loading && <div className="loading">Загрузка...</div> } { state.results.error && <div className="error">Произошла ошибка. Возможно, сервер перегружен. Попробуйте еще раз.</div> } { state.availableTrains && <div className="results"> { state.availableTrains.map((train) => <div />) } </div> } </div> </div>);
});

Оператор nested позвозяет «раскладывать» данные по подъячейкам, для этого первым аргументом в формулу передается callback, с помощью которого можно «протолкнуть» данные в подъячейку (одну или несколько). Теперь у нас есть отдельные потоки, которые отвечают за ошибку, статус промиса и за полученные данные. Оператор nested — очень мощный инструмент и один из немногих императивных в mrr (мы сами указываем, в какие ячейки класть данные). В то время как оператор merge объединяет несколько потоков в один, nested расщепляет поток на несколько подпотоков, таким образом являясь его противоположностью.

Приведенный пример является стандартным способом работы с промисами, в mrr он обобщен в виде оператора promise и позволяет сократить код:

results: ['promise', (query) => getTrains(query.from, query.to, query.date), 'searchQuery'], // используем один из подпотоков availableTrains: 'results.data',

Также оператор promise следит за тем, чтобы использовались результаты только самого последнего промиса.

Компонент для отображения наличных мест (откажемся для простоты от разных типов вагонов)

const TrainSeats = withMrr({ selectSeats: [(seatsNumber, { id }) => new Array(Number(seatsNumber)).fill(true).map(() => ({ trainId: id })), '-seatsNumber', '-$props', 'select'], seatsNumber: [() => 0, 'selectSeats'],
}, (state, props, $) => <div className="train"> Поезд №{ props.num } { props.from } - { props.to }. Количество свободных мест: { props.seats || 0 } { props.seats && <div> Выберите необходимое Вам количество мест: <input type="number" onChange={ $('seatsNumber') } value={ state.seatsNumber || 0 } max={ props.seats } /> <button onClick={ $('select') }>Выбрать</button> </div> }
</div>);

Чтобы обращаться к props в формуле, можно подписаться на специальную ячейку $props.

const Tickets = withMrr({ ... selectedSeats: '*/selectSeats',
}, (state, props, $, connectAs) => { ... <div className="results"> { state.availableTrains.map((train, i) => <TrainSeats key={i} {...train} {...connectAs('train' + i)}/>) } </div>
}

Мы опять используем пассивное слушание чтобы подхватить количество выбранных мест при нажатии на кнопку «Выбрать». Каждый дочерний компонент мы связываем с родительским с помощью функции connectAs. Пользователь может выбрать места в любом из предложенных поездов, поэтому мы слушаем изменения во всех дочерних компонентах с помощью маски "*".

Как «аккумулировать» данные потока? Но вот незадача: пользователь может добавить места сначала в одном поезде, потом в другом, так что новые данные перетрут предыдущие. Для этого существует оператор closure, который вместе с nested и funnel составляет основу mrr (все остальные — не более чем синтаксический сахар на основе этих трех).

selectedSeats: ['closure', () => { let seats = []; // эта функция станет формулой return selectedSeats => { seats = [...seats, selectedSeats]; return seats; } }, '*/selectSeats'],

При использовании closure сначала (на componentDidMount) создается замыкание, которое возвращает формулу. Она таким образом имеет доступ к переменным замыкания. Это позволяет сохранять данные между вызовами безопасным способом — не скатываясь в пучину глобальных переменных и shared mutable state. Таким образом, closure позволяет реализовать функциональность таких операторов Rx, как scan и прочие. Однако этот способ хорош для сложных случаев. Если же нам нужно только сохранять значение одной переменной, мы можем просто использовать ссылку на предыдущее значение ячейки с помощью специального имени "^":

selectedSeats: [(seats, prev) => [...seats, ...prev], '*/selectSeats', '^']

Теперь пользователь должен ввести имя и фамилию для каждого выбранного билета.

const SeatDetails = withMrr({}, (state, props, $) => { return (<div>Поезд { props.trainId } <input name="name" value={ props.name } onChange={ $('setDetails', e => ['name', e.target.value, props.i]) } /> <input name="surname" value={ props.surname } onChange={ $('setDetails', e => ['surname', e.target.value, props.i]) }/> <a href="#" onClick={ $('removeSeat', props.i) }>X</a> </div>);
}) const Tickets = withMrr({ $init: { results: {}, selectedSeats: [], } stationFrom: 'selectStationFrom/val', stationTo: 'selectStationTo/val', searchQuery: [(from, to, date) => ({ from, to, date }), '-stationFrom', '-stationTo', '-date', 'searchTrains'], results: ['promise', (query) => getTrains(query.from, query.to, query.date), 'searchQuery'], availableTrains: 'results.data', selectedSeats: [(seats, prev) => [...seats, ...prev], '*/selectSeats', '^']
}, (state, props, $, connectAs) => { return (<div> <div> <OptionsInput { ...connectAs('selectStationFrom') } getOptions={ getMatchedStations } /> - <OptionsInput { ...connectAs('selectStationTo') } getOptions={ getMatchedStations } /> <input type="date" onChange={ $('date') } /> <button onClick={ $('searchTrains') }>Поиск</button> </div> <div> { state.results.loading && <div className="loading">Загрузка...</div> } { state.results.error && <div className="error">Произошла ошибка. Возможно, сервер перегружен. Попробуйте еще раз.</div> } { state.availableTrains && <div className="results"> { state.availableTrains.map((train, i) => <TrainSeats key={i} {...train} {...connectAs('train' + i)}/>) } </div> } { state.selectedSeats.map((seat, i) => <SeatDetails key={i} i={i} { ...seat } {...connectAs('seat' + i)}/>) } </div> </div>);
});

Ячейка selectedSeats содержит массив выбранных мест. По мере того, как пользователь вводит имя и фамилию к каждому билету, мы должны изменять данные в соотвествующих элементах массива.

selectedSeats: [(seats, details, prev) => { // ??? }, '*/selectSeats', '*/setDetails', '^']

Стандартный подход нам не подойдет: в формуле мы должны знать, какая именно ячейка изменилась и реагировать соответствующе. Нам поможет одна из форм оператора merge.

selectedSeats: ['merge', { '*/selectSeats': (seats, prev) => { return [...prev, ...seats]; }, '*/setDetails': ([field, value, i], prev) => { prev[i][field] = value; return prev; }, '*/removeSeat': (i, prev) => { prev.splice(i, 1); return prev; }, }, '^'/*, здесь также могут быть любые другие аргументы*/],

Это немного напоминает редьюсеры Redux'а, но с более гибким и мощным синтаксисом. И можно не бояться мутировать массив, ведь контроль над ним имеет только формула одной ячейки, соответственно параллельные изменения исключены (а вот мутировать массивы, которые передаются в качестве аргументов, конечно же не стоит).

Реактивные коллекции

Паттерн, когда ячейка хранит в себе и изменят массив, очень распостранен. Все операции с массивом при этом бывают трех типов: вставка, изменение, удаление. Чтобы описать это, существует элегантный оператор "coll". Используем его чтобы упростить вычисление selectedSeats.

Было:

selectedSeats: ['merge', { '*/selectSeats': (seats, prev) => { return [...prev, ...seats]; }, '*/setDetails': ([field, value, i], prev) => { prev[i][field] = value; return prev; }, '*/removeSeat': (i, prev) => { prev.splice(i, 1); return prev; }, 'addToCart': () => [], }, '^']

стало:

selectedSeats: ['coll', { create: '*/selectSeats', update: '*/setDetails', delete: ['merge', '*/removeSeat', [() => ({}), 'addToCart']] }]

при этом формат данных в потоке setDetails нужно немного изменить:

<input name="name" onChange={ $('setDetails', e => [{ name: e.target.value }, props.i]) } /> <input name="surname" onChange={ $('setDetails', e => [{ surname: e.target.value }, props.i]) }/>

Используя оператор coll, мы описываем три потока, которые будут влиять на наш массив. При этом поток create должен содержать сами элементы, которые должны быть добавлены в массив (обычно — объекты). Поток delete принимает либо индексы элементов, которые нужно удалить (как в '*/removeSeat'), так и маски. Маска {} удалит все элементы, а, к примеру, маска { name: 'Carl' } удалила бы все элементы с именем Carl. Поток update принимает пары значений: изменение, которое нужно сделать с элементом (маска либо функция), и индекс или маску элементов, которые нужно изменить. Например, [{ surname: 'Johnson' }, {}] установит фамилию Johnson всем элементам массива.

Оператор coll использует что-то вроде внутреннего языка запросов, позволяя упростить работу с коллекциями и сделать ее более декларативной.

Полный код нашего приложения на JsFiddle.

Довольно существенная тема, которая осталась за бортом — глобальное управление состоянием, возможно она будет рассмотрена в следующих статьях. Мы ознакомились практически со всем необходимым базовым функционалом mrr. Но уже сейчас вы можете начать использовать mrr для управления состоянием внутри компонента или группы родственных компонентов.

Выводы

В чем сила mrr?

mrr позволяет писать приложения на React в функционально-реактивном стиле (mrr можно расшифровать как Make React Reactive). mrr очень выразителен — вы тратите меньше времени на написание строчек кода.

Также есть инструменты для расширения этого базового набора (возможность создавать кастомные операторы). mrr предоставляет небольшой набор базовых абстракций, которого достаточно для всех случаев — в этой статье описаны практически все основные фичи и приемы mrr. mrr сам представляет собой гигантскую монаду, отделяющую чистые вычисления от мутаций состояния. Вы сможете писать красивый декларативный код, не читая сотни страниц мануала и даже не изучая теоретические глубины функционального программирования — врядли вам придется использовать, скажем, монады, т.к.

Комфорт, удобство, простота, экономия времени программиста — основные преимущества mrr (отсюда еще одна расшифровка mrr как «мр-р-р», то есть мурчание удовлетворенного жизнью кота). В то время как в других библиотеках зачастую соседствуют разнородные подходы (императивный с помощью методов и декларативный с помощью реактивных биндингов), из которых программист произвольно смешивает «салат», в mrr существует единая базовая сущность — поток, что способствует гомогенности и единообразию кода.

Каковы минусы?

Программирование «строками» имеет как свои преимущества, так и недостатки. У вас не будет работать автокомплит имени ячейки, а также поиск места, где она определена. С другой стороны, в mrr всегда есть одно и только одно место, где определяется поведение ячейки, и его несложно найти простым текстовым поиском, в то время как поиск места, где определяется значение поля Redux стора, или тем более поля state при использовании нативного setState, может быть более долгим.

Кому это может быть интересно?

В первую очередь, адептам функционального программирования — людям, для которых преимущество декларативного подхода очевидно. Конечно, уже существуют кошерные решения на ClojureScript, но все же они остаются нишевым продуктом, в то время как React правит бал. Если в вашем проекте уже используется Redux, вы можете начать использовать mrr для управления локальным состоянием, и в перспективе перейти к глобальному. Даже если вы не планируете использование новых технологий в данный момент, вы можете разобраться с mrr чтобы «размять мозг», взглянув на привычные задачи в новом свете, ведь mrr существенно отличается от распостраненных библиотек управления состоянием.

Это уже можно использовать?

В принципе, да 🙂 Библиотека молодая, пока что активно использовалась на нескольких проектах, но API базового функционала уже устоялось, сейчас работа ведется преимущественно над разными примочками (синтаксический сахар), призванными еще больше ускорить и облегчить разработку. К слову, в самих принципах mrr нет ничего специфично React'овского, его возможно адаптировать для использования с любой компонетной библиотекой (React был выбран в силу отсутствия у него встроенной реактивности или общепринятой библиотеки для этого).

Спасибо за внимание, буду благодарен за отзывы и конструктивную критику!

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»