Пожалуйста, авторизуйтесь Это третья и заключительная часть статьи про разработку изоморфного React.js приложения с нуля. Части первая и вторая. В этой части мы: Это очень удобная библиотека, упрощающая процесс разработки. С ее помощью вы сможете в режиме реального времени видеть содержимое глобального состояния, а также его изменения. Дополнительно redux-dev-tools позволяет "откатывать" последние изменения глобального состояния, что удобно в процессе тестирования и отладки. Нам же она добавит наглядности и сделает процесс обучения более интерактивным и прозрачным. Во второй части мы поместили создание корневого редьюсера в configureStore, что не совсем правильно, так как это не его зона ответственности. Сделаем небольшой рефакторинг и перенесем его в redux/reducers/index.js. Из документации redux-dev-tools следует, что нам необходимо внести изменения в configureStore. Вспомним, что инструменты redux-dev-tools нам нужны только для разработки, поэтому повторим маневр, описанный ранее: Реализация configureStore.dev.js с DevTools и поддержкой hot-reload. Точка входа configureStore Все готово! Перезапускаем webpack-dev-server и nodemon, открываем браузер и видим, что справа появилась панель, которая отражает содержимое глобального состояния. Теперь откроем страницу со счетчиками и понажимаем на ReduxCounter. Одновременно с каждым кликом мы видим, как в очередь redux поступают действия и глобальное состояние изменяется. Нажав на Revert, мы сможем отменить последнее действие, а нажав на Commit — утвердить все действия и очистить текущую очередь команд. Примечание: после добавления redux-dev-tools, возможно, вы увидите сообщение в консоли: "React attempted to reuse markup in a container but the checksum was invalid...". Это означает, что серверная и клиентская часть приложения рендерят неодинаковый контент. Это очень плохо, и в своих приложениях таких ситуаций следует избегать. Однако, в данном случае виновником является redux-dev-tools, который мы все равно в продуктиве использовать не будем, поэтому можно сделать исключение и спокойно проигнорировать сообщение о проблеме. Update: спасибо пользователям gialdeyn и Lerayne, починить SSR с redux-dev-tools можно следующим способом  Реализуем следующий сценарий Это достаточно объемная задача. Чтобы сфокусироваться на отдельных ее частях, сначала реализуем пункты 1,2 и 5, а для 3 и 4 сделаем заглушку. После клика по кнопке "Запросить время" мы должны последовательно: Здесь каждое действие мы оформляем в виде небольшой функции, которая будет изменять глобальное состояние, а timeRequest — комбинация этих функций, которая целиком описывает наш сценарий. Именно ее мы и будем вызывать из нашего компонента. Добавим кнопку react-bootstrap-button-loader с поддержкой индикатора загрузки на страницу TimePage и научим ее вызывать функцию timeRequest по клику. Заметим, что нам пришлось использовать connect из react-redux, чтобы у нашей кнопки был доступ к функции dispatch для изменения глобального состояния. Самое время посмотреть на результаты трудов: откроем страницу "Время" в браузере, нажмем на кнопку "Запросить". Интерфейс пока еще ничего не делает, но в redux-dev-tools мы теперь видим, как запускаются actions, которые мы совсем недавно реализовали. Настало время оживить интерфейс. Начнем с реализации логики для обновления глобального состояния Важный момент, о котором нельзя забывать: по спецификации redux мы не имеем права изменять переданное нам состояние и обязаны возвращать либо его же, либо новый объект. Для формирования нового объекта я использую Object.assign, который берет исходный объект и применяет к нему нужные мне изменения. Хорошо, теперь добавим новый редьюсер в корневой редьюсер. Снова откроем браузер и, предварительно очистив очередь redux-dev-tools, покликаем по кнопке "Запросить". Интерфейс все еще не обновляется, но теперь наши actions изменяют глобальное состояние согласно коду нашего редьюсера, а это значит, что "под капотом" вся логика работает как надо. Дело за малым — "оживим" интерфейс. Переходим в браузер, снова нажимаем на кнопку "Запросить" и убеждаемся, что все работает согласно нашему сценарию. Настало время заменить заглушку на настощий backend. Примечание: для этого примера я использую очень простой backend, разработанный мной на rails. Он доступен по ссылке https://redux-oauth-backend.herokuapp.com и содержит только один метод /test/test, возвращающий серверный timestamp, если пользователь авторизован, иначе — 401 ошибку. Исходный код backend'а можно найти тут: https://github.com/yury-dymov/redux-oauth-backend-demo. Там я использую gem devise для авторизации, который де-факто является стандартом для решения подобных задач для rails и gem devise_token_auth, добавляющий devise механизм авторизации Bearer Token-based Authentication. В наши дни этот механизм чаще всего используется при разработке защищенных API. Со стороны клиентской части нам многое предстоит сделать: Механизм очень простой: Как видно из кода, глобальное состояние я передаю в переменной REDUX_INITIAL_STATE. Устанавливаем redux-oauth Примечание: мы используем redux-oauth для изоморфного сценария, но она также поддерживает и client-side only. Примеры конфигурации для различных случаев и демо можно найти на сайте библиотеки. Примечание 2: redux-oauth использует cookie для авторизации, так как механизм local storage не подходит для изоморфного сценария. Активируем плагин cookieParser для express Настраиваем redux-oauth для серверной части приложения Здесь происходит много интересного: Согласно документации, нам также необходимо добавить редьюсер redux-oauth в корневой редьюсер. Функция fetch из redux-oauth — это расширенная функция из пакета isomorphic-fetch. Согласно документации, ее необходимо вызывать через dispatch, так как в этом случае у нее будет доступ к глобальному состоянию, из которого она сможет считать авторизационный токен и отправить его вместе с запросом. Если функцию fetch использовать для произвольного HTTP-запроса, а не запроса к API, то авторизационный токен использован не будет, то есть алгоритм ее выполнения на 100% совпадет с алгоритмом выполнения isomorphic-fetch. Примечание: isomorphic-fetch — это библиотека, которая умеет делать HTTP-запросы как из браузера, так и из Node окружения. Откроем браузер и еще раз нажмем на кнопку "Запросить" страницы "Время". Что ж, мы больше не видим текущий timestamp, зато в redux-dev-tools появилась информация о 401 ошибке. Неудивительно, ведь мы должны быть авторизованы, чтобы API нам что-то вернул. Как правило, авторизованный пользователь имеет больше возможностей по работе с системой, чем гость, иначе какой же смысл в авторизации? С технической точки зрения это означает, что многие компоненты могут выглядеть и вести себя по-разному в зависимости от того, зашел пользователь в систему или нет. Я являюсь ярым сторонником принципа DRY (don't repeat yourself), поэтому напишем небольшой хелпер. Реализуем кнопку "Войти в систему" Эта кнопка будет отображаться, только если пользователь еще не вошел в систему. Реализуем кнопку "Выйти из системы" Эта кнопка будет отображаться, только если пользователь уже вошел в систему. Я добавлю авторизацию на страницу HelloWorldPage. Настало время насладиться результатами нашего труда. Нажимаем на кнопку "Войти", используем свой github аккаунт для авторизации и… мы в системе! Кнопка "Войти" исчезла, зато появилась кнопка "Выйти". Проверим, что сессия сохраняется, для этого перезагрузим страницу. Кнопка "Выйти" не исчезла, а в redux-dev-tools можно найти информацию о пользователе. Отлично! Пока все работает. Переходим на страницу "Время", нажимаем на кнопку "Запросить" и видим, что timestamp отобразился — это сервер вернул нам данные. На этом можно было бы закончить, но нам нужно "отшлифовать" наше приложение. Итак, что можно улучшить: Открываем браузер и видим, что ссылка на страницу "Время" все еще доступна, переходим на страницу HelloWorldPage, нажимаем на кнопку "Выйти" — и ссылка пропала. Как мы помним, за соответствие между URL и страницей, которую нужно отрендерить отвечает библиотека react-router, а конфигурация путей находится в файле routes.jsx. Нам нужно добавить следующую логику: если пользователь неавторизован и запросил защищенную страницу, то перенаправим его на HelloWorldPage. Для получения информации о пользователе нам необходимо передать в routes.jsx ссылку на хранилище глобального состояния. Тестируем: Примечание: в функции requireAuth используется setTimeout с нулевой задержкой, что на первый взгляд лишено смысла. Это сделано специально, так как позволяет обойти баг в одном из популярных браузеров. Если поступит action SIGN_OUT, то все данные редьюсера timeReducer будут заменены на initialState, то есть на значения по умолчанию. Этот же прием необходимо реализовать для всех других редьюсеров, которые содержат пользовательские данные. Библиотека redux-oauth поддерживает Server Side API requests, то есть в процессе рендеринга сервер может сам обратиться к API за данными. Это имеет множество преимуществ: Примечание: да, поисковики не будут авторизовываться, но некоторые сервисы API смогут возвращать данные и для неавторизованных пользователей с некоторыми ограничениями. redux-oauth подойдет и для таких сценариев. Реализуем небольшой Proof of Concept. Добавим запрос к API в серверную часть нашего приложения После того, как функция initialize из redux-oauth обратится к backend, проверит авторизационный токен и получит данные о пользователе, мы выполним запрос timeRequest на стороне сервера. После его выполнения мы отрендерим контент и отдадим ответ пользователю. Откроем браузер, авторизуемся при необходимости, перейдем на страницу "Время" и нажмем F5. Мы должны увидеть timestamp, хотя кнопку "Запросить" никто не нажимал. Если открыть Dev Tools браузера, вкладку Network и повторить эксперимент, то мы увидим, что запроса к API из клиента не было. Это подтверждает, что вся работа была сделана на стороне сервера. Внесем последнее небольшое улучшение в наш проект: будем делать запрос к API только в том случае, если пользователь авторизован. Как мы видим, внутри возвращаемой функции мы можем получить доступ к актуальному глобальному состоянию посредством вызова функции getState, которая будет передана вторым аргументом. Об этом далеко не все знают, а это очень полезная возможность. Вот и подошел к концу цикл статей о веб-приложении на React.js с нуля. Искренне надеюсь, что он был вам полезен! С удовольствием отвечу на ваши вопросы в комментариях, а также принимаю запросы на темы для следующих статей. Ссылка на проект на github — https://github.com/yury-dymov/habr-app/tree/v3 P.s. Если в тексте присутствуют ошибки или неточности, пожалуйста, напишите мне сначала в личные сообщения. Заранее спасибо! Full Stack And Mobile Senior Developer