ZarahioN Presents

Answering why

React

Почему саги? React + Redux-saga. [Pt. 1]

Учитывая, что ты заглянул в запись с таким названием, что такое React уже знаешь. Шутка, не зря сами создатели назвали библиотеку redux-saga, а не react-saga — многие принципы и способ работы базируется вокруг именно редакса, который, является достаточно свободной библиотекой. Однако, чтобы понять принципы работы достаточно знать только о транзакциях (action\effect).

Поэтому, надеюсь ты знаешь что такое Redux.
(Но ничего, даже если React и Redux вызывают только дрожь и страдания, о них тоже будет понятно и доходчиво, как-нибудь в будущем)

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

И вместе с тем дополнительные проблемы для разработчиков, ибо саги хотят генераторы:

function* fetchQuote(action) {
    try {
        const quote = yield call(apiCall, action.data);
        yield put({type: "QuoteFetchSucceeded@Saga", quote: quote});
    } catch (e) {
        yield put({type: "QuoteFetchFailed@Saga", message: e.message});
    }
}

В глаза сразу бросается вопрос — «за что?!».

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

К примеру такой простой счетчик:

function* counter(){
    let i = 1;
    while(true)
        yield i++;
}
let count = counter();
var a = setInterval(() => console.log(count.next()), 500)

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

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

Учитывая эти два элемента — приостанавливаемое выполнение и сохранение при этом состояния исполнения — можно легко понять почему генератор был выбран для написания саг. Сохранение состояний позволяет писать комплексные запросы совершающие несколько действий, а возможно использовать паузу дает чувство ностальгии по великолепному sleep(1).

Что же, после этого короткого посвящения в ужасы прелести генераторов можно вернуться к сагам:

function* fetchQuote(action) {
    try {
        const quote = yield call(apiCall, action.data);
        yield put({type: "QuoteFetchSucceeded@Saga", quote: quote});
    } catch (e) {
        yield put({type: "QuoteFetchFailed@Saga", message: e.message});
    }
}

Первая остановка — yield call() — мы хотим сделать долгий запрос — отправляем функцию на исполнение, а, когда придет результат, продолжаем обработку, за счет возврат результата в quote (в генератор можно отправить что-то, вызвав .next(variable), значение при этом возвращается из yield на котором остановился генератор).

Таким образом мы приостанавливаем работу генератора пока идет долгий запрос, а когда он наконец завершается, saga (библиотека) доброжелательно возвращает нам результат выполнения запроса.

Call — просто вспомогательная функция от библиотеки, которая выполняет переданную в нее функцию сразу же возвращая нам простой объект (Эффект — Effect), что может быть важно, к примеру, при юнит-тестах. Никто не мешает просто напрямую сделать запрос.

Следующая остановка — yield put({type: "QuoteFetchSucceeded@Saga", quote: quote});

А вот это уже похоже на отправку Redux действия (dispatch(action)). Функция put указывает saga что нужно создать действие для редакса — вызвать dispatch с переданным объектом. Ожидаемо действие проходит, передавая с собой «цитату»:

{
    "type(pin)": "QuoteFetchFailed@Saga",
    "quote(pin)": "UnAPI-ed quote text, cause Yoda text converter we wanted to use got bonked and refused to respond with anything other than 503"
}

К сожалению, мы протестировали наш try-catch блок, к счастью — он работает.

Как только мы подходим к put в saga, мы переходим в царство редакса — а на его тему нужны совсем другие уроки. Впрочем, остался еще один «непростой» момент в работе с сагами — выбор селектора.

function* mySaga() {
    yield takeLatest("QuoteFetchRequest@Saga", fetchQuote);
    // vs
    yield takeEvery("QuoteFetchRequest@Saga", fetchQuote);
}

export default mySaga;

Есть два стандартных варианта: takeLatest и takeEvery. Их сигнатуры совпадают (ActionTypeHandler, WorkerGenerator), первое — название действия, которое ты будешь создавать из редакса, второе — генератор, который мы недавно разбирали — функция, которая будет последовательно обрабатывать это действие.

Согласно названиям они имеют важное отличие — takeLatest обработает (возьмет) только результат последнего отправленного запроса, takeEvery — все результаты. Это бывает полезно, когда пользователи спамят кнопки, а тебе слишком лень блокировать кнопку на время запроса.

…И присоединение этого селектора к redux store:

import mySaga from './saga'
// 1.
const sagaMiddleware = createSagaMiddleware()

let composer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
    reducer,
    composer(
        // 2.
        applyMiddleware(
            sagaMiddleware
        )
    ),
)
// 3.
sagaMiddleware.run(mySaga)

Что же, пора приступить к решающим вопросам: почему saga и почему не saga?

Почему саги

В этом будет легко убедить стоящих горой за TDD, последовательность, читаемость и развернутость — используя саги нужно хорошенько постараться, чтобы написать запутанный и логически сложный код, следуя хоть каким-то стандартам.

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

Сага решает проблему сложных действий внутри компонента:

handleButtonClick(){
    let data = this.state.variable;
    request.post(API_URL, data)
    //+ установить в состояние что идет работа и возможно произвести другие вызовы к интерфейсу
    .then(response => this.props.responseRecieved(response))
    .catch(errorHandler)
}
connect(
    ...,
    ..responseRecieved(data) => dispatch({type: RESP_TYPE, data})
)()
@render
<button onClick={handleButtonClick}>Press me!</button>

// или снося вес в действие
connect(
    ...,
    ..requestStarted(data) => (dispatch) => {
        apiCall(data)
        .then(response => dispatch({type: REQ_TYPE, data}))
        .catch(errorHandler)
    }
)
@render
<button
    onClick={
        () => this.props.requestStarted(this.state.variable)
    }
>Press me!</button>

После скромного опыта с сагами мне как-то.. некомфортно было это писать, мне даже кажется что с thunk и запросами в компоненте это делается по-другому…

Альтернатива с использованием саги:

@render
<button
    onClick={() => this.props.requestStarted(this.state.variable)}
>Press me!</button>

connect(
    ...,
    ..requestStarted(data) => dispatch({type: REQ_TYPE, data})
)

@saga
function* requestData(action) {
    try {
        const result = yield call(apiCall, action.data);
        yield put({type: RESP_TYPE, quote: quote});
    } catch (e) { errorHandler(e) }
}

function* reqSaga() {
    yield takeLatest(REQ_TYPE, requestData);
}

Почему не саги

  • Генераторы это «страшно»
  • Многословность и лишний файл, от этого никак практически не избавиться.
  • Это лишняя библиотека.

Буду ждать больше причин за и против в комментариях, ну а я, если в будущем меня столкнет с Redux, точно возьму с собой и Saga.

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

Исходный код полностью доступен в репо, история коммитов представляет порядок создания приложения вместе с написанием статеек.

Отправка параметров и несколько одновременных запросов. React + Redux-saga. [Pt. 2]

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

Но это скучно.

Ведь мы все хотим совершать десятки запросов по несколько мегабайт каждый. Приступим.

function* fetchQuote(action) {
    try {
        const quote = yield call(requestYodified, action.text);
        const spellcheckedQuote = yield call(spellcheck, quote.data);

        yield put({type: success, quote: quote.data, spellcheckData: spellcheckedQuote.data});
    } catch (e) {
        yield put({type: fail, message: e.message});
    }
}

Как можно заметить, изменения по сравнению с прошлой версией огромны.

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

К сожалению апи от Йоды опять полетел именно в тот момент, когда я попытался проверить работу последовательных запросов.

Ненадежен, Йода, ты же как…

Поэтому почему бы не расширить немного обработку ошибок — к примеру, чтобы совсем не разочаровывать пользователя сообщением о ошибке, мы проверим как у него с орфографией!

...
catch (e) {
    try {
        const spellcheckedQuote = yield call(spellcheck, action.text);
        yield put({
            type: spellchecked, results: spellcheckedQuote.data
        });
    }
    finally {
        yield put({type: fail, message: e.message});
    }
}

Таким образом мы можем, к примеру, вывести результат выполнения части запросов, если это более выгодно по сравнению с полной остановкой из-за ошибки где-то на промежуточном этапе работы. А за время работы над «неудачным» сценарием, Йода поднялся и приготовился к работе — можно отдать ему какой-нибудь текст для перевода в человеческий — к примеру, заголовок нашего приложения «Welcome to React sagas!», спустя несколько долгих секунд раздумий Йода наконец отвечает «To react sagas welcome! Herh herh herh.». Вот и замечательно.

    {
    type: 'QuoteFetchRequest@Front',
    text: 'Welcome to React sagas!'
    }

    {
    type: 'QuoteFetchSucceeded@Saga',
    quote: 'To react sagas welcome! Herh herh herh.',
    spellcheckData: {
        original: 'To react sagas welcome! Herh herh herh.',
        suggestion: 'To react sagas welcome Hersh her her.',
        corrections: {
        Herh: [
        'Hersh', ...
        ],
        herh: [
        'her.', ...
        ] } }
    }

С последовательными запросами мы разобрались — добавить еще десяток-другой в цепочку, чтобы заставить пользователя ждать минутами, дело техники и сокровенной комбинации ctrl+c -> ctrl+v. Осталось научиться делать одновременно много, как некогда почивший салат.

function* fetchQuoteWhilSpellchecking(action) {
    try {
        const [quote, spellcheckedText] = yield all([
            call(requestYodified, action.text),
            call(spellcheck, action.text)
        ]);

        yield put({type: success, quote: quote.data, spellcheckData: spellcheckedText.data});
    } catch (e) {
        yield put({type: fail, message: e.message});
    }
}

Как вновь видно — изменений очень много.

Функция all — очередной вспомогательный элемент saga, позволяет создать несколько запросов и, дождавшись выполнения каждого, продолжить выполнение саги с полученными значениями. Запись вида const [a, b, c] = [...] — альтернатива деструктуризации для массивов, ожидаемо переводит массив вида [3, 2, 4] в три переменные — a = 3, ...

Наяривание до десятка запросов опять же происходит старым, добрым и немого уставшим ctrl+c -> ctrl+v. Но помни, пользователь очень любит ждать и обладает наилучшим железом, чем больше количество и чем толще запросы выше качество запросов — тем лучше.

Совмещение последовательного и параллельного выполнения опять же банально — получив результат параллельной обработки создаем новый запрос, и так далее, смешивая в нужных пропорциях.

В следующем выпуске — Саги: интересные части. Эффекты, отложенное исполнение, комбинирование и предсказуемое ожидание действий.

Код опять же доступен в репо, помимо полного кода, там появился «индикатор» загрузки.

Эффекты и саги. React + Redux-saga. [Pt. 3]

Я должен признаться, я был не совсем откровенен, называя call или put вспомогательными функциями (и насчет еще некоторых моментов, но забудем пока про это).

call, put и многие другие являются фабриками Эффектов (Effect) — специальных объектов, напоминающих действия (action) из redux, только предназначенные для самой библиотеки saga.

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

Как я уже упомянул, эффекты сильно напоминают собой действия:

{
  CALL: {
    fn: log,
    args: [42],
    context: null
  }
}

Так, к примеру, выглядит возврат из call (немного обрезанный, но тем не менее). Как видно «тип» эффекта прописан ключом объекта.

Впрочем к чему весь этот разговор — эффекты, вспомогательные функции или фабрики, какая разница?

Знакомьтесь, наша новая фабрика take:

function* typing(action) {
    try {
        yield put({type: typingStart});
        yield take(typingStopped);
        yield put({type: typingEnd});
    } catch (e) {
        yield put({type: "Welp, we did it"});
    }
}

После написания этого примера, я начинаю понимать, насколько

В любом случае, фабрика («вспомогательная функция» мне нравилось больше) take позволяет дождаться действия редакса изнутри обработчика. Применения? Бесконечны! О чем и говорит мой креативный пример.

Как? Мы же в мире генераторов. Исходный код я, конечно же, не смотрел, но предполагаю что saga перенаправляет вызов .next нашего обработчика-генератора на появление указанного действия и «БАМ!» совершенно магическим образом мы получаем это действие внутри обработчика как только оно появляется благодаря dispatch-у.

На самом деле это очень удобный инструмент для поддержания нашей иллюзии синхронности — мы можем взять любое действие — то же начало ввода пользователем — сделать что-то — изменить интерфейс, к примеру — и продолжить выполнение саги уже после другого действия. Более привычный подход заключается в разделении логики на два обработчика — вход в состояние по первому действию и выход из него по второму (более сложная десятистраничная логика приветствуется с распростертыми объятиями).

Однако, текущий вариант не слишком отличается от классического подхода и достаточно бесполезен в чистом виде, но это не конец. saga позволяет помимо прочего, использовать элементы «многопоточного» программирования — fork или race, помимо некоторых других.

Мы даже уже использовали all, который позволяет дождаться выполнения всех эффектов (обещаний) и получить все результаты в один массив. В saga есть его близкий родственник — race. Они похожи, только вот race ждет завершения любого из эффектов и возвращает объект с именованным результатом:

function* typing(action) {
try {
    while (true){
        yield take(textUpdated);
        yield put({type: typingStarted});
        while (true){
            const {updated, timeouted} = yield race({
                updated: take(textUpdated),
                timeouted: call(delay, 1000),
            })
            if (timeouted) {
                yield put({type: typingEnded});
                break;
            }
        }
    }
} catch (e) {
    yield put({type: "Welp, we did it"});
}
}

Как ни пугающе, но у нас теперь два бесконечных цикла (и скорее всего способ переписать это в более презентабельном виде).

Первый цикл нужен для прослушивания действия textUpdated. Как только мы его получили, можно вступить во второй цикл, перед которым мы отправляем в редакс сообщение, что пользователь начал печатать. Внутри мы создаем условие гонки — или пользователь продолжает печатать — updated— и мы ничего не делая перезапуская внутренний цикл, или проходит 1 секунда — delay(1000[ms]) — и мы отправляем сообщение, что пользователь перестал печатать и выходим во внешний цикл прослушивания.

Почему это лучше?

Несколько причин:

  1. Нам больше не нужно такое в компоненте:
start="1.">
clearTimeout(this.typingTimeout);
this.typingTimeout = setTimeout(() => typingStopped(), 1000);

И нам даже не нужно вмешиваться в обработчик в компоненте — он точно так же просто продолжает создавать действие, что был введен новый текст, и ничего более.

  1. Код отвечающий за состояние печатания выглядит проще (за исключением двух циклов while(true) которые все же немного портят картину) и находится в одном единственном месте.
  2. Нам не нужна еще одна библиотека чтобы сделать задержку или отложенное действие. К примеру, если использовать all вместо race то в результате у нас получится массив со всеми собранными за секунду действиями — очень напоминает буфер из RxJS и подобных. Комбинируя несколько простых эффектов можно получить неплохой набор часто используемых инструментов.

start="2.">

Почему нет?

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

На этом, я думаю, наш небольшой цикл уроков по redux-saga завершается. Осталось пару фабрик и вспомогательных функций, которые мы не разобрали, вроде fork, join или cancel, однако они имеют схожее поведение и очень похожи на свои аналоги из мира многопоточного программирования. Если нужда в их разборе появится, то мы несомненно этим займемся.

Опять же — код доступен в репоа документация по сагам на их сайте.

React-Saga «продвинутый» пример.

За время отсутствия записей я какое-то время посвятил созданию небольшого приложения-таймера, логику которого я реализовал практически полностью с помощью саг.

And that was a pleasant walk in a lush park, I must say.

Приступим:

export const types = {
    'start': 'StartTimer@Front',
    'stop': 'StopTimer@Front',
    'pause': 'PauseTimer@Front',
    'resume': 'ResumeTimer@Front',
    'stopped': 'TimerStopped@Saga',
    'timeUpdated': 'TimerUpdated@Saga',
}

Для начала действия: всего пользователь может отправить 4 действия (они помечены сферой @Front[end]) — старт и стоп таймера, чтобы начать его выполнение с самого начала, или пауза-продолжение — чтобы соответсвенно приостановить, без сбрасывания счетчика, и продолжить его выполнение. Также есть 2 действия (@Saga), которые производит сама сага: стоп, чтобы оповестить редуктор и сам таймер что пора остановиться и обнулить состояние, и обновление текущего времени.

Можно заметить, что я как и ранее работаю вопреки принятому стандарту в виде капитализированных констант. Также я использую два типа наименования: verb + noun — для действий, которые начинают какой-то процесс, как правило приходящие от пользователя, и noun(gerund) + past verb — для действий, которые должны напрямую влиять на состояние приложения — когда действие уже произошло и нужно оповестить элементы приложения и пользователя.

Feel free to translate it into classic redux types definitions for yourself.

Next stop:

function* ticking(action) {
    try {
        let timeElapsed, pomoTime, restTime;
        while (true){
            const { started } = yield race({
                started: take(types.start),
                unpaused: take(types.resume),
            });
            if (started) {
                pomoTime = started.pomoTime;
                restTime = started.restTime;
                timeElapsed = 0;
            }
            while (true) {
                let timePrev = Date.now();
                const { cancelled } = yield race({
                    ticked: call(delay, Math.round(Math.random() * 8) + 16),
                    cancelled: take([types.pause, types.stopped]),
                });
                if (cancelled)
                    break;
                timeElapsed += Date.now() - timePrev;
                if (timeElapsed > (pomoTime + restTime) * 1000)
                    yield fork(stopTimer);
                yield put({ type: timeUpdated, elapsed: timeElapsed });
            }
        }
    } catch (e) {
        yield put({type: "Welp, we did it"});
    }
}

Нас встречает уже знакомая конструкция:

const { started } = yield race({
    started: take(start),
    unpaused: take(types.resume),
});

Для запуска таймера мы ожидаем одно из двух действий — он только начал работу (types.start) или продолжил с паузы (types.resume). При этом в первом случае мы берем себе действие, чтобы знать о необходимости обнулить состояние таймера.

if (started) {
    pomoTime = started.pomoTime;
    restTime = started.restTime;
    timeElapsed = 0;
}

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

Текущее с поправками — из-за принципа работы saga select выполняется после обработки действия редуктором, если он конечно есть, что может привести к неожиданному результату при получении данных. Подробнее об этом.

let timePrev = Date.now();
const { cancelled } = yield race({
    ticked: call(delay, Math.round(Math.random() * 8) + 16),
    cancelled: take([pause, types.stopped]),
});

race2; it was, yeah it was until 63aa3….

Мы создаем «гонку», чтобы дать возможность пользователю остановить таймер. Элемент случайности добавлен сугубо для эстетического эффекта — цифры увеличиваются в хаотичном порядке, а не одинаковыми множителями в 8\16\33 миллисекунды.
Завершение парада:

if (cancelled)
    break;
timeElapsed += Date.now() - timePrev;
if (timeElapsed > (pomoTime + restTime) * 1000)
    yield fork(stopTimer);
yield put({ type: timeUpdated, elapsed: timeElapsed });

Если мы получили остановку — выйти из цикла работы таймера while(true). Я использую Date.now() вместо простой арифметики для капельку большей точности — если планировщик saga пропустит пару тиков из-за пика загрузки у пользователя, таймер продолжит работать с относительной точностью. Это сугубо экспериментальный ход, так как я не тестировал ни эффективность планировщика saga, ни работоспособности и точности таймера при пиковых нагрузках.

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

Я использую fork из-за одной интересной особенности кода и планировщика саг — если просто использовать call и остановить таймер, то сигнал о остановке может придти слишком рано и тогда наш таймер его пропустит, заметит что время позднее и надо останавливать уже во второй раз, пропустит сигнал снова и снова попытается остановится — и таким образом повесит всю страницу.
Поэтому приходится использовать небольшую задержку — достаточно большую, чтобы все эффекты saga успели отправиться, но достаточно маленькую, чтобы таймер не успел подумать что его никто не будет останавливать (16-24 мс в нашем случае).
Как часто бывает, я уверен что есть более элегантное решение этой проблемы, но на момент написания статьи я его не придумал, если знаешь чем можно помочь — добро пожаловать в комментарии или репо.

function* stopTimer() {
    yield delay(5);
    yield put({type: timeUpdated, elapsed: 0});
    yield put({type: types.stopped, finished: true});
}

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

Что же, на этом пока все, весь код доступен в репо, а на рабочий экземпляр посмотреть (и сломать) можно на этой страничке.

Разбираемся с React(Webpack)-ом [Pt. 1 — Manual React Installation (and pain of doing so)]

Что вообще такое React(.js)?

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

И это большей частью хорошо, не учитывая какой вес дополнительных библиотек и страшных слов тянет за собой эта аббревиатура. (Да-да, я знаю насколько многие боятся redux, не говоря о redux-saga, которые бок о бок частенько ковыляют на просторах вакансий React.js разработчиков). Впрочем, все не так страшно(, и часто очень даже приятно). Поэтому пристегиваемся и отправляемся в путь, ну а чтобы поездка прошла легко, начнем со слона аккуратно прячущегося на заднем плане.

Начало работы с React.js

Есть два (с половиной) основных способа начать использовать реакт, не учитывая банальную загрузку готовой библиотеки, но этот способ не для нас.

Первым и уже практически ставшим стандартом, является использование утилиты create-react-app, которая создаст базовую установку для практически мгновенного начала работы со стандартным и привычным набором инструментов.

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

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

Что же нам нужно?

Внимание, путь дальше очень страшен и труден, придется столкнутся с терминалом или командной строкой!
Существует альтернативный простой путь, но я все же рекомендую пройти базовую подготовку и «боевое крещение».

Node.js и npm, а для любителей котиков и чуть более упорных людей — еще и yarn

Процесс установки обоих достаточно прост и стандартизирован для главных ОС. После установки можно убедиться в успешности введя в терминал или командную строку:

node -v
npm -v

И для вышеназванных любителей котиков:

yarn -v

Если все команды успешно выполнились и вывели ожидаемые версии — успех! Первый шаг сделан.

Для чего нам Node.js?

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

Следующим шагом нам нужно установить сам реакт и пару сопутствующих инструментов:

npm(yarn) init
Круглые скобки подсказывают взаимозаменяемость команд или флагов

Это создаст специальный файл package.json, в котором хранится описание, версия и набор необходимых для приложения библиотек. Во время инициализации скорее всего придется ответить на пару вопросов о приложении.

npm i[nstall] -S(--save) react react-dom либо yarn add react react-dom
Квадратные скобки подсказывают полное название команды или флага

Это должно установить библиотеки react и react-dom, обе необходимые для работы реакта в браузере. И технически мы могли бы начать работу (на некоторых версиях реакта) прямо сейчас, используя src="node_modules/react/dist/..." но это не очень удобно и совсем не то, как мы будем в будущем работать.

Следующим шагом будет установка специального инструмента — «переводчика» (transpiler) или же просто компилятора babel. Он нужен главным образом из-за использованного в реакте языка JSX, который представляет собой XML с вписанным в атрибуты обычным JavaScript-ом. О нем мы еще позже подробно поговорим, но просто знай, что он помогает упростить жизнь, переводя более современный и удобный вариант JS в поддерживаемый повсеместно (и в некоторых областях сильно устаревший) JS.

Мы могли бы установить инструмент babel-cli, который позволяет в реальном времени преобразовывать код, однако, в мире мало кто использует его таким образом для чего-то серьезнее прототипа или приложения на вечер. Поэтому помимо babel мы добавим еще одно некогда страшное слово в список библиотек — webpack.

Webpack позволяет собирать сложные проекты из множества файлов-модулей и используемых библиотек в один большой исполняемый файл, который можно загрузить на страницу.
(При необходимости можно собирать несколько небольших исполняемых файлов и используемые библиотеки выносить отдельно, но это уже вопрос оптимизации процесса сборки)

npm i -D(--save-dev) webpack@3.11.0 babel-loader babel-core babel-preset-react-app
либо
yarn add -D(--dev) webpack@3.11.0 babel-loader babel-core babel-preset-react-app
-D или --dev это флаг для записи библиотек в условный раздел devDependencies нашего package.json. Условный он потому, что для запуска приложения нам не нужны и зависимости из dependencies — они будут собраны в готовый файл в будущем. Однако это полезное разделение на относящиеся непосредственно к работе приложения модули и просто необходимые для сборки (и желательно тестирования) инструменты.

Это загрузит непосредственно webpack и babel. Важный момент — проверь, что вебпак установился нужной версии. На протяжении его разработки было несколько критических изменений в структуре файла конфигурации, которые отказывались от обратной совместимости и нужно было обновлять файл конфигурации. Если у тебя установится версия, которая внесла подобные изменения в отношении к указанной, предложенный далее файл конфигурации может не работать. Плюс конфигурация вебпака была в некоторой мере «неодназначной» и в связи с обилием примеров на просторах интернета под прошлые версии — сломать что-то и гадать, почему оно не работает, было очень легко.
Сейчас ситуация стала куда лучше, но избежать возможных проблем на первых шагах все же не помешает.

Что же заглянем на страницу документации настройки вебпака и.. да…

Но, к счастью, большая часть настроек нам не понадобиться в ближайшее (и, скорее всего, дальнейшее) время, однако знать, куда попробовать заглянуть в случае чего — весьма полезно. Полный процесс настройки я, возможно, распишу как-нибудь в другой раз, сейчас же хватит простенького конфига для сборки базового приложения на реакте. (Мы почти готовы писать код спустя пол часа подготовки!)

@webpack.config.js
var path = require('path');
var webpack = require('webpack');

module.exports = {
    entry: './index.js',
    output: {
        path: path.resolve(__dirname, 'public'),
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /\.jsx?$/,
                loader: 'babel-loader',
                options: {
                    presets: ['react-app']
                }
            }
        ]
    },
    resolve: {
        modules: [path.resolve(__dirname, 'node_modules')],
        extensions: ['.js', '.jsx']
    },
};

Для начала что такое require и module.exports — это функционал по соответственно импорту и экспорту модулей и библиотек. Он начал свой путь с библиотек вроде RequireJS, потом перекочевал в Node.js и JS как расширенная часть языка и наконец был принят в стандарте ES6. (Или как-то так, я не слишком хорошо осведомлен о истории модулей).

Далее непосредственно конфиг:
entry — файл, с которого начинать сборку, с него по сути начинает работу приложение — загружает при необходимости библиотеки (реакт, к примеру) и что-то делает со страницей.
output — путь и название конечного файла (или файлов) в которые соберется весь необходимый код приложения.
module — «логика» (rules) вебпака — какие файлы обрабатывать, через какие загрузчики (loader) их пропускать, какие параметры передать этим загрузчикам и так далее.
— — test — регулярное выражение (как правило) по которому проверять, какие файлы должен обрабатывать этот загрузчик
— — loader — непосредственно название библиотеки загрузчика
— — options — набор параметров, который нужно передать данному загрузчику, каждый загрузчик принимает свой собственный набор параметров
resolve — какие пути (modules) проверять при импорте модулей в коде и какие расширения файлов считать импортируемыми (extensions) таким образом. Это немного своеобразный элемент, так как он (должен) работать только для импорта библиотек, но можно настроить подгрузку своих файлов, чтобы не нужно было прописывать глубоких путей наверх в некоторых случаях. Это своеобразный вопрос конфигурации и я, возможно, разберу его при необходимости в будущем.

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

Но, мы еще не закончили, последний шаг.

Hello World with React.js (and bunch of not so fancy stuff)

Создадим два пустых файла index.html и index.js (который мы указали в entry конфига вебпака). В body html файла нужно добавить две строки:

<div id="root"></div>
<script src="bundle.js"></script>

Элемент div нужен как контейнер (или обертка) для указания реакту, куда вставлять (render) разметку его компонентов. Скрипт же очевидным образом загружает bundle.js, который мы сейчас соберем вебпаком из index.js. Переходя к которому:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

const StateLessComponent = ({text}) =>
<div>
    <span>{text}</span>
</div>;

class StateFullComponent extends Component {
    state = {
        text: 'Heya Earthlings!'
    }
    render(){
        return (
            <StateLessComponent text={this.state.text}/>
        );
    }
}

ReactDOM.render(<StateFullComponent/>, document.getElementById('root'));

И примерно так выглядит весь React-код. Конечно же, разметки, логики и финтифлюшек в реальной жизни куда больше, но база остатется той же. Смотрим снизу-вверх:

ReactDOM.render(JSXComponent, DOMNode) — вставляет (монтирует) собранный реакт(jsx)-компонент в указанный элемент DOM-а страницы. То есть, мы находим какой-то элемент (контейнер) на реальной странице и вставляем в него наш компонент (или дерево вложенных компонентов, как в нашем скромном случае).

class StateFullComponent extends Component — это ES6 синтаксис-обертка для создания функции-конструктора и по совместительству в реалиях реакта — полноценный компонент.
Полноценный означает что он может, помимо простого отображения разметки (метод render), иметь некую логику (свои методы\функции), иметь состояние (state) и использовать некоторые «события» реакта. (lifecycle hooks, технически это не события, но тем не менее, практическая суть достаточно близка).
О всех этих несомненно интересных словах мы поговорим, уже разбирая непосредственно внутренности реакта.

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

const StateLessComponent = ({text}) => <div>...

(arguments) => (returnedValue) это небольшое но приятное дополнение ES6 имеющее, помимо некоторых очень нужных и важных свойств, возможность записывать функции в коротком виде.

Разделение на statefull и stateless, «умные» и «глупые», контейнеры и компоненты мелькает очень часто, однако простая теоретическая суть — умные имеют логику, данные, и всякое и раскидывают это все по глупым, которые занимаются только отображением — очень часто не суть реальная. Подходов и способов работы со всей этой кутерьмой множество и основную часть мы рассмотрим, как выше было сказано, ковыряя сам реакт.

Последние строки:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

Это уже названный выше функционал модулей в ES6 версии JS — он немного отличается от использованного в конфиге вебпака из-за того, что даже Node.js еще не начал полноценное использование этого стандарта.
(Насколько я знаю*. Достаточно скоро или уже Node.js может полностью перейти к этому стандарту, но до тех пор там используется CommonJS версия импорта и экспорта — require() и module.)

Помимо синтаксического отличия я использовал именованный импорт — он позволяет запросить только нужные части из модуля, без всего модуля.
И опять же требуется пометка — насколько я знаю, на данный момент для такого (реально) частичного импорта нужно использовать «обрезание» кода и дерева модулей — «dead code elimination» и «tree shaking», по этим словам ты скорее всего найдешь информацию, если тебя интересует вопрос оптимизации размеров твоего приложения. Я же, возможно, в будущем сам расскажу подробнее про эти возможности, но учитывая какой уровень дополнительной сложности вносит только базовый вебпак, оптимизация — это разговор на другой раз.

Поэтому мы наконец завершаем наше затянувшееся путешествие, во время которого мы успели
1. [Поставить Node.js, npm и, опционально, но мягко рекомендуемо, yarn.](#Что же нам нужно?)
2. Создать пустой проект через yarn init и добавить в него реакт через yarn add
3. Немного разобрать что есть babel и webpack и зачем они нужны
4. [И даже написать первые строки приложения на реакте](#Hello World with React.js (and bunch of not so fancy stuff))

На этом я ненадолго оставляю тебя с, надеюсь, немного подкипевшей головой и списком материала на вечернее чтение:
npm, yarn, webpack, babel и сам реакт
Это сайты документации или помощи инструментов, которыми мы будем очень часто пользоваться на пути создания реакт приложений и я, как бы не хотел, не смогу ответить на все вопросы — иногда придется расчехлить навыки поиска и покопаться в официальных источниках (ну или заглянуть на stackoverflow\подобные) в поисках нужного ответа.

Благость под названием create-react-app [Pt.2 – Automatic React Installation (and how to break it)]

Что же, в прошлый раз мы исследовали прелестное искусство мазохизма и настраивали своеобразный синоним среды разработки самостоятельно — с вебпаком, бейблом и ручной настройкой всего и вся. Но еще в самом начале прошлого рассказа я упомянул, о куда более простой альтернативе:

create-react-app

Что же это? По факту cli — command line interface для создания базового шаблона стандартного приложения на реакте.

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

Почему не сразу? Все просто — чтобы научиться что-то чинить, нужно или это что-то сломать, или построить свой вариант. Ломать это, конечно, весело, но без хоть какого-либо понимания, процесс обратной починки может затянуться, а то и вовсе по факту не начаться. Если у тебя что-то сломается при использовании шаблона созданного c-r-a — ты хотя бы примерно сможешь ориентироваться в каком из узлов всей цепочки сборки и компиляции это произошло и где искать помощи.

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

Впрочем, завершим затянувшееся лирическое отступление и приступим к делу:

Работа с create-react-app

Есть два пути: старый-добрый-глобальный, если у тебя npm ниже версии 5.2, и новый-модный npx, если выше. Отличаются они в одну строчку и кучку деталей (про которые можно почитать здесь(англ) или здесь(рус)), поэтому посмотрим оба (, а я таки обновлю свою стареющую ноду 8.5.0 версии (хотя это делать оказалось необязательно)).

Классический способ это установка cli в глобальную директорию npm:

npm i -g create-react-app

После чего можно использовать: (если это первая глобальная установка, возможно придется добавить путь в PATH на Win или в /bin (или куда нужно) на *nix системах)

create-react-app app-folder-name

Это создаст проект в «app-folder-name» по текущему пути консоли.

Модный способ это использование вышеназванного npx, который поставляется с 5.2 версий npm:

npm install npm@latest -g

Это обновит npm, на случай, если ты хочешь использовать npx, но npm уже старенький. Напоминаю, что проверить версии можно с помощью npm(node) -v

npx create-react-app app-folder-name

Это опять же создаст проект в «app-folder-name» по текущему пути консоли.

Удобство npx состоит в том, что он позволяет как запускать локально установленные версии инструментов (модулей), так и использовать «одноразовые» инструменты вроде create-react-app без их установки в глобальную директорию. Что в последнем случае очень удобно — не приходится следить за версией названных инструментов.

И.. собственно все, в этот мы справились куда быстрее, не так ли? Мы можем запустить вебпак через yarn start и начать создавать!

Но это скучно.

Поэтому, приготовься покинуть кабину, мы катапультируемся!

(Я знаю, что eject это не совсем катапультироваться, но тем не менее)

create-react-app это очень базовый, стандартизированный и проверенный способ начинать работу с реактом, поэтому в нем нет всех тех прикольных штук, которые тебе возможно захочется использовать. К примеру последний писк моды и ES8 с компиляцией от бейбла, или немногословный и немножко страшный, но такой удобный stylus, или тебе просто, как и мне, интересно поковыряться и узнать как они все это настроили.

Поэтому без промедления (и находясь в папке с созданным проектом):

yarn eject

Ах, сколько новых папочек и файлов

К слову, если ты (как любой адекватный человек, да-да) держишь проект в гит-репо, c-r-a не даст тебе катапультироваться без сохранения последних изменений, что очень мило с их стороны, на случай если тебе не понравится опыт досрочного кхем выпуска.

Что же, разберемся по-подробнее с появившимся разнообразием конфигурационных файлов. В особенности меня сейчас интересует configs/webpack.config.dev.js — он содержит конфигурацию для — yarn start — процесса разработки, а ближайшее время мы будем заниматься только им.
К слову, в нем достаточно много подробных комментариев, так что приглашаю заглянуть в него вместе, не дожидаясь моих пояснений.
Также помимо самого файла конфигурации, может быть полезно заглянуть в configs/paths.js — он содержит распознаватель путей и непосредственно их самих — полезно примерно прикидывать что куда указывает.

module.exports = {
  entry: [
    require.resolve('react-dev-utils/webpackHotDevClient'),
    paths.appIndexJs,
  ],
  output: {
    pathinfo: true,
    filename: 'static/js/bundle.js',
    chunkFilename: 'static/js/[name].chunk.js',
    publicPath: '/',
  },
  resolve: {
    modules: ['node_modules', paths.appNodeModules].concat(
      process.env.NODE_PATH.split(path.delimiter).filter(Boolean)
    ),
    extensions: ['.web.js', '.mjs', '.js', '.json', '.web.jsx', '.jsx'],
  },
  module: {
    strictExportPresence: true,
    rules: [
      {
        test: /\.(js|jsx|mjs)$/,
        enforce: 'pre',
        use: [
          {
            options: {
              formatter: eslintFormatter,
              eslintPath: require.resolve('eslint'),
            },
            loader: require.resolve('eslint-loader'),
          },
        ],
        include: paths.appSrc,
      },
      {
        oneOf: [
          {
            test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
            loader: require.resolve('url-loader'),
            options: {
              limit: 10000,
              name: 'static/media/[name].[hash:8].[ext]',
            },
          },
          {
            test: /\.(js|jsx|mjs)$/,
            include: paths.appSrc,
            loader: require.resolve('babel-loader'),
            options: {
              cacheDirectory: true,
            },
          },
          {
            test: /\.css$/,
            use: [
              require.resolve('style-loader'),
              {
                loader: require.resolve('css-loader'),
                options: {
                  importLoaders: 1,
                },
              },
              {
                loader: require.resolve('postcss-loader'),
                options: {
                  ident: 'postcss',
                  plugins: () => [
                    require('postcss-flexbugs-fixes'),
                    autoprefixer({ ... }),
                  ],
                },
              },
            ],
          },
          {
            exclude: [/\.(js|jsx|mjs)$/, /\.html$/, /\.json$/],
            loader: require.resolve('file-loader'),
            options: {
              name: 'static/media/[name].[hash:8].[ext]',
            },
          },
        ],
      },
    ],
  },
};

Я, как можно заметить, обрезал комментарии и некоторые слишком уж специфичные элементы оптимизации или работы вебпака, с которыми шансы столкнуться приближается к скромному нулю (плюс плагины, опции загрузчиков и бонусы для react-native).

Как можно сразу заметить, entry и output слегка отличаются от нашего самопала, однако большей частью это лишь косметические изменения:

entry может принимать не единственную строку, как в нашем случае, а массив или даже объект строк (и массивов, вложенность, ура), в данном случае помимо нашего приложения вебпак просто обрабатывает сервер для разработки и набор заполнителей выбранный c-r-a; output же использует несколько доп опций:
pathinfo — включает в конечную сборку данные о путях, модулях и прочем,
publicPath — позволяет указать корень выхода — папку, в которую помещаются все сгенерированные файлы, мы не использовали его, так как наш проект содержал всего один файл
chunkFilename — указывает как называть чанки — куски кода оптимизированные для отдельной загрузки и кеширования.

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

И наконец module. Помимо самих загрузчиков, присутствует один параметр strictExportPresence — он указывает вебпаку расценивать не найденный модуль (импорт) как ошибку и сразу же прекращать дальнейшую сборку, что очень полезно, так как сразу же кричит о проблеме, не дожидаясь перезагрузки пака и порой, если используется роутер или компонент загружается не сразу, попадания на пропущенный импорт.

С загрузчиками, впрочем, все достаточно просто и знакомо (спасибо вебпаку за стандартизацию конфигурации):
Первым идет eslint — линтер для JS. Линтер — это «инструмент статического анализа» — или говоря по-человечьи, подсветка ошибок и несоответствий выбранному набору правил написания кода. Очень полезный инструмент, который, что особенно приятно, как правило доступен в виде расширения для редактора.
При этом загрузчик линтера использует специальный модификатор enforce — он нужен, чтобы линтер всегда обрабатывал исходный код до его преобразования бейблом. Модификатор (параметр) enforce задает категорию порядка применения загрузчика, всего доступно 4 опции (pre, inline, normal, post), о которых можно почитать здесь.

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

На чем наш расковыряный файл конфигурации заканчивается. Я, опять же, мягко указываю тебе покопаться в конфигурации самостоятельно и, к примеру, добавить поддержку любимого css препроцессора или пресета для бейбла. Процесс достаточно простой, и я буду разбирать его в будущем, когда приложению понадобится стилизация (а делать ее на css или тем более встроенных стилях я отказываюсь), но всегда полезно попробовать самостоятельно (имея бекап), прежде чем идти читать руководство.
Также должен заметить, что учитывая базовость и частое нежелание катапультироваться из c-r-a, создали кучку способов изменения или расширения конфигурации, оставаясь технически с c-r-a. Но я сам больше склоняюсь к eject-у, нежели использованию таких инструментов.

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

Первые серьезные строки на React.js [Pt.3 – Finally starting for real]

В прошлых монотонных рассказах мы занимались долгой и утомительной настройкой вебпака и вообще «среды» для создания React приложений.

К счастью, это мучение наконец закончилось и мы можем приступить к созданию нашего приложения. Ура, свобода!

Впрочем, начнем с азов, затронутых еще в первом рассказе. Заглянем в файл index.js, он скорее всего находится в папке src, если ты продолжаешь с пути c-r-a, либо прямо в корне проекта, если решил в лоб принять удар тяжелой и героической самостоятельной настройки вебпака.

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

Для простоты подхода (и соблюдения традиций) мы начнем с глупых — stateless — компонентов.
На данный момент существует 3 способа их создания, впрочем последний (и исторически первый) через React.createClass мы не будем разбирать как технически устаревающий (и не зря же мы проходили через все страдания вебпака и бейбла)

class X extends React.Component

import React, { Component } from 'react'; 
import ReactDOM from 'react-dom'; 

class ColoredDiv extends React.Component {
    render() {
        const {color, fontSize: fz} = this.props;
        return <div style={{background: this.props.bgColor, color, fontSize: fz}}>Me colored, yay!</div>
    }
}

ReactDOM.render(<ColoredDiv bgColor="green" color="red" fontSize="14px"/>, document.getElementById('root'));

Синтаксис классов это одно из тех первых нововведений ES6, которые встречаешь начиная изучать реакт, он по сути является прямым наследником названного React.createClass. Как правило классы создают только для умных компонентов, однако технически наш ColoredDiv остается глупым, так как у него нет никакой логики или состояния, и сейчас его можно напрямую преобразовать в функциональный.

Разберем же наш простенький код по полочкам:
import X from 'x'; — механизм загрузки и сборки модулей, для которого мы и используем вебпак, так как это до сих пор достаточно далекий от браузерного стандарта функционал. Как можно правильно догадаться, import React from 'react' дает доступ к функционалу реакта посредством константы React (также любой импорт говорит вебпаку загрузить указанную библиотеку, что бывает важно в некоторых случаях).
class X extends React.Component — синтаксис для создания «класса» в ES6, при этом мы наследуем от базового класса реакта (потому что надо).
render() это метод нашего класса, который будет использовать реакт для создания разметки компонента — ее рендера.

this.props это специальный неизменяемый объект (свойство\параметр), в который реакт помещает переданные компоненту свойства (prop(ertie)s).

Непосредственно процесс передачи можно увидеть глянув на последнюю строчку — ReactDOM.render. Первым аргументом для него идет JSX разметка (технически возврат React.createElement, но это ненужные детали). Мы использовали имя созданного компонента (класса) и вставили несколько атрибутов: bgColor, color, fontSize эти атрибуты и попадают в объект props компонента.

Как можно заметить, в рендере самого компонента мы возвращаем JSX разметку подобную той, что передавали в ReactDOM.render (наблюдаешь связь?). И как можно заметить, фигурные скобки в атрибутах служат для передачи JS значений, включая объекты как в style (отчего там получились по две фигурные скобки, внешние для обозначения компилятору что идет JS код, внутренние уже для самого объекта), так и просто строки или числа.

Также я использовал сразу три способа использования переменных из свойств (props): доступ напрямую this.props.bgColor, и деструктуризации {color} = this.props и {fontSize: fz} = this.props, третья отличается от второй лишь тем, что мы переименовали переменную перед использованием. Как правило используется второй и третий тип, так как это позволяет сразу в начале рендера указать, какие свойства используются в компоненте.

Как можно догадаться, фигурные скобки позволяют использовать JS не только в атрибутах:

render() {
    const {color, fontSize: fz } = this.props;
    return (<div style={{background: this.props.bgColor, color, fontSize: fz}}>
        {Math.random() >= 0.5 ? 'Me colored, yay!' : 'Me no random!'}
    </div>)
}

Помимо скромных забав со случайными числами таким образом можно использовать любой JS код, который возвращает значения (строки, числа, элементы реакта (JSX) или массивы перечисленных типов), к повсеместно используемому примеру, Array.map:

class ColoredDiv extends React.Component{
    render() {
        const {color, fontSize: fz, rows } = this.props;
        return (<div style={{background: this.props.bgColor, color, fontSize: fz}}>
            {rows.map(row => <li>{row}</li>)}
        </div>);
    }
}

ReactDOM.render(<ColoredDiv 
    color="#ab00ff" 
    bgColor="white" 
    fontSize="14px" 
    rows={[
        'Me colored, yay!', 
        'Me colored, nope second try!', 
        'No random is it!'
    ]}
/>, document.getElementById('root'));

Как можно ожидать (или нет, с непривычки) — ColoredDiv выведет уже знакомый стилизованный div и li со строками из массива переданного через свойство rows (ReactDOM.render).
Если ты еще не ознакомился с функциональными методами массивов, постарайся разобраться хотя бы с минимальным набором map, filter, reduce, и опционально forEach, последний технически идентичен map, просто не возвращает значений. Но особенно непредсказуемо полезным может оказаться reduce. А первые два ты скорее всего будешь использовать повсеместно, работая с реактом (и очень вероятно без него тоже).

Помимо бездумного разглядывания кода, я предлагаю тебе самостоятельно побаловаться с нашим скромным примером, использовав все три перечисленных типа данных доступных для вывода, и в целом привыкнуть к подобию JSX на HTML и его отличительным особенностям, которые позволяют творить (иногда к сожалению) что угодно за счет вставки чистого JS кода в разметку. Особенно интересно становится, когда ты заканчиваешь играть со стандартными HTML елементами и начинаешь компоновать разметку компонентами реакта.

Итак, что мы успели сделать:
1. Создали класс-компонент ColoredDiv через class ColoredDiv extends React.Component { ... },
2. Добавили в него метод render() { ... } который возвращает JSX разметку (<div style={{ ... }}>...</div>),
3. Отрендерили (или правильнее сказать монтировали (mount)) созданный компонент посредством ReactDOM.render(<ColoredDiv ... />, realDOMElem),
4. Передали свойства (props) компоненту ColoredDiv через ReactDOM.render и использовали их.

Базовое знакомство с работой реакта? Завершено.

Хоть это и скучно, но на сейчас почти хватит, осталось только глянуть второй способ записи глупых компонентов, о котором я говорил ранее:

const X = (props) => …

Как можно заметить, имея опыт с ES6, это просто «стрелочная» (arrow) функция. Такой тип записи компонентов называется функциональным (duh!) и является более простой и удобной заменой относительно громоздкому классу. К слову, использовать стандартные функции теоретически тоже можно, но лично я такое не практикую.

Также я упоминал, что наш чрезмерно неумный компонент можно без дополнительных манипуляций преобразовать в функциональный:

const ColoredDiv = ({color, fontSize: fz, rows, ...props }) => 
<div style={{background: props.bgColor, color, fontSize: fz}}>
    {rows.map(row => <li>{row}</li>)}
</div>

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

На этой недовольной лирической ноте мы завершаем знакомство с практическими азами реакта.. и начинаем наш для некоторых короткий, а для некоторых очень долгий путь познания современных реалий веб-разработки с React.js.

Почему вообще Реакт? [Pt.4 – Why bother]

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

Я как-то наскоком погнал ставить вебпак, ваять реакт и даже не поудосожился предложить один очень интересный вопрос: а зачем он вообще?

Для ответа на него, думаю, стоит вернуться к «реальному программированию» на ПК — «прикладному программированию оконных приложений» (desktop applications), это технически все пользовательские программы на Windows и macOS. С Linux все немного интереснее, но даже там «оболочку» приобретают все больше приложений с ростом базы обычных пользователей.

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

Однако немножко я все же знаю и попробую доходчиво пояснить.

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

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

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

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

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

Все становится еще немного интереснее, когда ты узнаешь, что помимо MVC есть практически, но сильно не совсем, такие же подходы вроде MVP или MVVM, и у каждого может быть по несколько разных вариаций.
Надеюсь я достаточно тебя запугал и запутал.

Flux на спасение (глуповатых мартышек)!

Удивительно, но не я один страдал от неочевидности и можно сказать искусственной сложности привычного подхода, особенно того хаоса, что часто царил в веб-разработке. Поэтому компания добра ™ Facebook представила (и скорее всего втихоря у себя использовала некоторое время) Flux-архитектуру. Из своего скромного опыта я бы сказал, что она похожа на доработанный и функциональный MVVM, но не буду, так как они все достаточно схожи.

Что же из себя представляет флакс (флукс? флякс? фляжка.. Flux!)? Это однонаправленная архитектура построения приложений, включающая Представление (Вид, View), Состояние (Модель, Store) и связку Действий и Диспетчера (Action+Dispatcher, не совсем, но в некотором роде Контроллер).
В отличии от MVC, где, в некоторых трактовках и подходах, Представление могло общаться как с Моделью так и с Контроллером, иногда еще и напрямую обновляя первую за счет действий пользователя, во Flux Представление получается за счет декларативного преобразования Состояния, Состояние же образуется за счет изменений созданных Диспетчером, которые в свою очередь вызывают Действия, которые могут быть созданы Представлением (пользователем) либо самой системой.

Таким образом получается прямой цикл взаимодействия: Действия указывают какие изменения должен создать Диспетчер изменив тем самым Состояние, исходя из которого получится новая версия Представления. Этот цикл работы прекрасно иллюстрирует график самого фейсбука:

]

Должен признаться, я скорее всего был плохим программистом, когда только столкнулся с необходимостью создавать интерфейсы на C#, я частенько путался и не был уверен, что, куда, зачем и почему. Не то чтобы я стал сильно лучше сейчас (я бы назвал себя смышленой мартышкой, пожалуй) но модель флукса я понял и принял мгновенно. И.. думаю это именно то, чего мне всегда так не хватало при попытке создать прикладное приложение (и декларативного реакта в виде представления, но это уже очень приятные мелочи).

Вспоминая о Реакте, технически он может представлять собой всю систему флукса: очевидно в основе он Представление, однако помимо этого есть Состояние (умные statefull компоненты), возможность его менять — Диспетчер (функционал по обновлению Состояния) — и в конце-концов Действия, которыми можно представить простые объекты переданные в Диспетчер. Однако, это хоть и удобный порой способ, при практически любой сколь немного сложной компоновке (или не дай Ишвар работы в команде), он быстро перестает быть хоть сколь эффективным.
Тем не менее, скоро мы его разберем, как базис для пробы пород флукса и поиск проблем, почему реакт сам по себе таки не выход.

На сим, я думаю, стоит завершать наш небольшой неполноценный экскурс в историю и готовиться продолжать работу с настоящим, с реактом. Ура.
И до встречи.

Пора умнеть [Pt. 5 — React Statefull Components, variation Uno]

В прошлый раз мы относительно успешно посмотрели на глупые или же stateless компоненты реакта и.. они на то и глупые, чтобы быть достаточно скучными и простыми. Настоящее веселье (или мучение, это как посмотреть) начинается с умными компонентами.

Небольшие правки были совершены в трактовке и тавтологии понятий 4 Апреля 2018

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

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

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

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

Таким образом терминология получается несколько сложнее, чем просто разделение есть состояние/нету, или есть функции/нету, однако такой набор правил позволяет чуть строже и надёжнее разграничить области влияния глупых и умных компонентов.
И как всегда — это лишь то разделение, к которому я пришел со временем, поглядывая на Flux, Redux и прочих влиятельных игроков на поле деятельности. Каждый волен использовать свои понятия, до тех пор, пока их ясно осознают все члены команды.

Что же, думаю, пора кончать болботать и начинать создавать. Начнем с простенького, форма:

@index.js
import ...
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));

@App.js
import React, { Component } from 'react';
import './App.css';

const LabeledInput = ({children, name, value, onChange, type="text"}) =>
<div className="input-group">
    <label htmlFor={name}>{children} </label>
    <input type={type} id={name} name={name} value={value} onChange={onChange}/>
</div>

class App extends Component {
    // constructor(p) {
    //     super(p);
    //     this.state = {
    //         demo: '',
    //         name: '',
    //         password: ''
    //     }
    //  this.handleInput = this.handleInput.bind(this) // привязка при использовании методов класса
    // }
    // handleInput(e) {
    //  this.setState({[e.target.name]: e.target.value})
    // }
    state = {
        demo: '',
        name: '',
        password: ''
    }
    render() {
        const handleInput = (e) => this.setState({
            [e.target.name]: e.target.value
        });
        const {
            demo, name, password
        } = this.state;
        return (<div className="form-container App">
            <form action="" onSubmit={(e) => e.preventDefault()}>
                <LabeledInput value={demo} name="demo" onChange={handleInput}>
                    First input label
                </LabeledInput>
                <LabeledInput value={name} name="name" onChange={handleInput}>
                    Namu?
                </LabeledInput>
                <LabeledInput value={password} name="password"onChange={handleInput} type="password">
                    Passwort
                </LabeledInput>
            </form>
            <div class="output">
                {
                    Object.entries(this.state).map(arr =>
                        arr[1] !== '' && <div>{arr[0]}: {arr[1]}</div>
                    )
                }
            </div>
        </div>);
    }
}

export default App;

Как можно сразу заметить — я вынес код приложения из index.js. Это стандартная практика — в index.js помещают конфигурацию и вызов ReactDOM.render корневого (root) элемента приложения, который часто является провайдером для роутера, состояния или их обоих.

В самом приложении (App.js) находится сразу два компонента — глупый для отображения отдельного поля в форме и умный для самой формы — LabeledInput и App соответственно. Помимо уже знакомых и вручную переданных через JSX атрибуты свойств (props), в LabeledInput присутствует еще одна деструктуризированная переменная children.

Children это специальное свойство (элемент объекта this.props), в которое реакт передает всех потомков JSX элемента:

<LabeledInput value={demo} name="demo" onChange={handleInput}>
    First input label
</LabeledInput>

В данном случае это просто текст «First input label» между открывающим и закрывающим тегом.
Сюда можно передавать любой JSX элемент — простой текст, будь то прописанный нами литерал или переменная, стандартные HTML элементы и другие JSX компоненты. При этом компонент получивший таких потомков сам решает что с ними сделать — в нашем случае мы просто вывели их в label, однако при желании и должной сноровке можно, к примеру, обработать, отфильтровать, или использовать их для каких-либо проверок внутри компонента.

Следующей примечательной остановкой является переменная\поле класса — state. Согласен, удивительно, но это именно то, что отличает statefull компоненты от stateless.
По сути state это объект (как правило; технически это просто переменная JS) представляющий «состояние» компонента — все данные нужные для его внутренней работы. Это чисто наш объект — реакт никак не вмешивается в его работу и узнает о его изменениях только в случае предназначенного обновления через специальный метод компонента setState. Альтернативно можно использовать forceUpdate() и прочие непотребства, но естественно это не рекомендуется в обычной практике.

Должен заметить что, насколько я помню, переменные класса не вошли в стандарт ES6 и могут быть недоступны в зависимости от настроек бейбла, впрочем, я могу ошибаться. Однако, если я не ошибся, то тебе возможно придется использовать классический синтаксис и инициализировать состояние через конструктор, я включил примерный код в виде комментария. Учитывая что наш класс расширяет компонент реакта, в самом начале конструктора нужно вызывать super передав в него полученные аргументом свойства (props) p. (Признаюсь, меня никогда не интересовало почему нужно так делать, поэтому объяснить это я не смогу)

Плавно переходим от создания базового состояния в виде пустых значений формы, к ее изменению. Тут придется немного напрячься — чтобы не писать одну и ту же функцию на каждое поле я задал один достаточно универсальный прототип:

const handleInput = (e) => this.setState({ [e.target.name]: e.target.value });

Это все та же любимая стрелочная функция, она получает исскуственный объект события и обновляет состояние установив по ключу в виде названия инпута (name) его значение (value). Я опять же использую один из ES трюков — переменную в виде ключа объекта.

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

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

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

  1. Классика и создание связывающей ссылки вроде переменной self, принцип простой — перед определением функции мы создаем «переменную» к которой привязываем нужный нам вариант this и используем уже эту переменную вместо прямого обращения к this, который был «перезаписан» функцией.
  2. Чуть более элегантный и признанный — создания «присвоенной» копии функции через специальный метод bind. Он позволяет указать что использовать функции в качестве this (или точнее просто привязать ссылку this внутри функции на то, что нужно нам). Помимо этого bind позволяет (и то зачем я куда чаще его использую) добавить аргументы в функцию, к примеру:

start="1.">
const reducingFunction = (incrementing , acc, elem) => (incrementing ? acc + elem : acc - elem)
let a = [1,2,3,4,5].reduce( reducingFunction.bind(null, true), 0 );
// a -> 15

Позволяет создать одну функцию для обработки нескольких типов данных или случаев применения. С таким простым вариантом как сумматор это, конечно, бессмысленно, однако позволяет делать интересные (и страшные) вещи с помощью простого reduce или forEach, возможно в будущем я покажу пару моих безобразных «изобретений». При этом я передал null первым аргументом bind, так как this внутри нашего сумматора мы не только не используем, но стрелочные функции даже не имеют понятия, что это.

Однако, возвращаемся после нашего краткого экскурса в страшные детали работы JS к доброму-мирному реакту.

Перед возвратом разметки можно заметить деструктуризацию значений для наших полей из состояния:

const { demo, name, password } = this.state;

Ничего особо нового, просто источник данных сменился со свойств (props) на состояние (state).

И наконец разметка, в которой все достаточно просто — у нас есть форма, внутри нее наши «глупые» компоненты которым передано все самое важное: значения и названия вводов(инпутов input), «ссылка» на обработчик и условная «подпись» поля через потомка. Условная потому что, как я уже говорил, поведение children определяет сам компонент — если вручную не вывести его где-либо в разметке, он вообще не станет отображаться.

Небольшим бонусом в самом конце я добавил вывод состояния через (очередную технически не полностью поддерживаемую) функцию (точнее статический метод, что не суть) Object.entries, обладающую двумя младшими братьями — Object.keys и Object.values, у них поддержка, кажется, чутка лучше. Суть у нее проста — взять объект и вернуть массив «кортежей» (но на самом деле массивов, мы же не развлекаем змеек) в виде [ключ, значение].
На тему поддержки — мы в царстве бейбла и технически можем о ней не волноваться, ибо все что нужно будет скомпилировано в простонародные варианты либо подкрашено заполнителями (polyfill), однако учитывая приятность огромного количества (человеческих, кхем) нововведений ES6 и выше, легко можно привыкнуть и писануть чего-нибудь ломающего браузер непросвещенного пользователя. Поэтому я стараюсь по ходу делать рассказывать о всем том, что это ломание любит вызывать и с чем я сам радостно сталкивался (тратя часы на отладку с пользователем устаревающего сафари, который при этом просто так не поставить себе для испытаний, я всегда недолюбливал яблоки).

Отставив стоны и стенания, запускаем
yarn start
И можем любоваться на нашу скромнейшую форму.
Но тыкать куда интереснее, поэтому не забудь ввести что-нибудь в каждое поле и порадоваться «магическому» обновлению небольшого отладочного вывода.

К слову о отладке, есть небольшой трюк для логгирования (и не только) в JS:

const LabeledInput = ({children, name, value, onChange, type="text"}) =>
console.log(children) || <div className="input-group">
    <label htmlFor={name}>{children} </label>
    <input type={type} id={name} name={name} value={value} onChange={onChange}/>
</div>

Так как console.log не возвращает значений он приравнивается к false и из-за поведения логического присваивания в JS функция возвращает не undefined а правильное значение после || — JSX разметку нашего компонента.

Итак, мое любимое повторение. Чего мы наваяли:
1. Создали полноценный компонент (класс) с помощью class X extends Component
2. Создали функциональный компонент для каждого именованного ввода с помощью стрелочной функции const Y = ({...props}) =>
3. В умном компоненте инициализировали состояние через поле класса state
4. В отображении (рендере) умного компонента создали обработчик полей ввода и деструктуризовали значения из его состояния
5. Вернули в умном компоненте разметку, в которой использовали функциональный компонент, передав его экземплярам данные и обработчик.
(6. Надеюсь, потискали получившуюся форму)

Что же, на этом, я думаю, пора завершать наше нескромно распухлевшее введение в умнеющие компоненты реакта. Я скорее всего предоставлю еще несколько примеров и пояснений классических вариантов использования реакта (в том числе покажу как можно реализовать отправку нашей формы и наукрашатильствовать ее по-максимуму) по ходу развития и рассусоливания всей темы.

Ну а пока — до встречи, надеюсь скорой.

Реакт, Флукс, Состояние, весело [Pt. 6 – Why states]

In previous chapter…

Что же, не так давно, в прошлой статье мы познакомились с Store — Состоянием (я о модели Flux) в базовом исполнении, который нам предлагает сам реакт. И появляется закономерный вопрос — а что с ним можно делать?

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

Например, мы можем при загрузке приложения включить интервал и получить, как ни странно, самый базовый игровой цикл (game loop):

class PseudoGame extends React.Component {
    state = { rotation: 0 }
    componentDidMount() {
        this.loop = window.setTimeout(this.loopFunction.bind(this), 16)
        // или requestAnimationFrame и подобные
    }
    loopFunction() {
        const { rotation } = this.state;
        this.setState({rotation: rotation + Math.random()})
        window.setTimeout(this.loopFunction.bind(this), 16);
    }
    render() {
        const { rotation } = this.state;
        return (
            <div style={{margin: '100px auto', width: 300, height: 300, border: '1px solid #ab00ff', transform: `rotate(${rotation}deg)`}} />)
        );
    }
}

Или реальная версия:

See the Pen React Rotating circle by Anton (@Askadar) on CodePen.

Такой незамысловатый код создаст неравномерно вращающийся квадрат и технически этот подход можно расширить до полноценной real-time игры с анимациями, движением и прочим. (Но без опыта лучше этого не делать и выбрать один из многочисленных доступных движков для создания игр на JS, при этом, я уверен, есть альтернативы в среде реакта)

Естественным образом вытекает уже данный ответ — мы вольны ваять из состояния все, что только пожелаем. Ведь как я уже не раз повторял — классическое состояние реакта это просто JS переменная, за которой реакт «следит» только за счет использования setState или вскользь упомянутого forceUpdate (, в практическом смысле, заглянуть под капот я пока не пытался).

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

За свою не очень долгую «карьеру» я успел, помимо банальных админок и форм, представить с помощью реакта шашки, в которые можно поиграть, zn парсер, текстовый markdown-подобный формат и даже черезмерно сложную и страшную систему проката, с таймерами, ценами и прочими математическими прелестями (которая оказалась немного неудобна из-за своей чрезмерной точности)

Однако, состояние в реакте хоть и является часто достаточным, но очень быстро становится неудобным и громоздким в некоторых критических случаях:
1. Вложенность\иерархия
2. Связи, особенно уровня братьев (siblings) или дети->родитель (children->parent)
Помимо того одним иногда неудобным недостатком состояния является императивный и нерасширяемый интерфейс setState.

Первая проблема — вложенность или сложная иерархия.

Под этой проблемой я подразумеваю большие деревья компонентов, которые очень часты в сложных приложениях. Сами по себе они достаточно безобидны, но что если, к примеру, мы создаем админку и нам нужно использовать имя пользователя в разных местах? Или нам просто нужно передать какие-то настройки всем (или большой части) компонентов?

Допустим мы храним его, то бишь имя пользователя, где-то достаточно высоко в иерархии компонентов, но тогда, чтобы показать его в сообщении приветствия одной из страниц или виджетов, нам придется спускать переменную с именем по всей цепочке от верхнего компонента до текста на странице, и в зависимости от размеров приложения и количества оберток путь может занимать пяток-другой промежуточных компонентов, в каждом из которых нужно передать это свойство дальше по цепочке. Хорошая перспектива, не находишь?

Для этой проблемы было два способа решения, просто передача всех не взятых свойств через ...rest и контекст, который не особо рекомендуют использовать сами разработчики реакта.
Рекомендация вполне простая — это апи не предназначено для обычной публики из пользователей библиотек — то есть по сути нас, оно предназначено для создателей библиотек. Собственно создатели и привнесли третий (или второй с половиной) способ решения проблемы, предоставив в начале Redux а позже и MobX с еще парочкой альтернатив — полноценных представителей состояния (Store) и в некоторой мере связки Диспетчера и Действий. Но о них чуть позже, перед этим:

Вторая проблема — связи между компонентами.

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

Представь такую воодушевляющую и прекрасную картину: мы все еще корпеем над нашей админкой, где кто-то зачем-то придумал писать персональное приветствие на каждом виджете. И, сюрприз, этот кто-то захотел помимо персонального приветствия показать аватарку человека, да при том при клике на аватарку открыть модальное окно профиля, которое у нас висит неподалеку от корня нашего приложения. Ну и естественно оно находится не в нашем дереве Content, а в заголовке страницы — компоненте Header.

- Root
- \ Header
- - \ Profile
- \ Content
- - \ Menu
- - \ Page
- - - \ Title
- - - \ HelloUser
- - - \ Article

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

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

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

Проблемы эти оказались очень популярны и решение (по совместительству полноценная реализация Состояния и практический пример подхода к вопросу Диспетчеров и Действия) пришло в виде библиотеки Redux, о которой сложно не услышать, начиная знакомство с реактом. За ней последовали MobX и еще парочка других менее заметных на данный момент.
Должен подчеркнуть — как Redux так и MobX большей частью являются независимыми от реакта сущностями и могут быть использованы где угодно в виде отдельного решения для хранения и управления состоянием, но так уж сложилось, что редакс стал практически неразлучным братом реакта на долгое время.

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

Redux, как и MobX мы будем подробнее разбирать в будущем, но я вкратце опишу как с первым решаются обе проблемы:

Проблема первая, вложенность или передача каких-то параметров вглубь дерева компонентов. Редакс предоставляет единое дерево состояния, части которого можно брать в любой компонент независимо от его положения в дереве компонентов (сколько деревьев). Таким образом можно создать объект User в дереве редакса и подключить его к компонентам Profile, HelloUser и любым другим, где нужны данные о пользователе.

Проблема вторая, связи между компонентами и тем более изменение данных других компонентов. Помимо предоставления данных редакс может поделиться своей реализацией создателя Действий, которые ловит редакс (Диспетчер) и передает в созданные нами обработчики, которые обновляют Состояние (ничего не начинает напоминать?). Таким образом проблема открытия модалки с профилем решается за счет выноса флажка «открыто ли» окна в дерево редакса и отправка действия на открытие этого окна идентичного из кнопки заголовка и страницы.

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

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

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

На этой серьезной ноте я завершу наш краткий экскурс в теорию Состояния, так и не ответив на последний скромный вопрос о недостатках встроенного в реакт версии. О них и отличиях между всеми вариациями правления чуть позже.