КулЛиб - Классная библиотека! Скачать книги бесплатно
Всего книг - 714798 томов
Объем библиотеки - 1415 Гб.
Всего авторов - 275165
Пользователей - 125191

Новое на форуме

Новое в блогах

Впечатления

Влад и мир про Тарханов: Мы, Мигель Мартинес (Альтернативная история)

Оценку не ставлю, но начало туповатое. ГГ пробило на чаёк и думать ГГ пока не в может. Потом запой. Идет тупой набор звуков и действий. То что у нормального человека на анализ обстановки тратится секунды или на минуты, тут полный ноль. ГГ только понял, что он обрезанный еврей. Дальше идет пустой трёп. ГГ всего боится и это основная тема. ГГ признал в себе опального и застреленного писателя, позже оправданного. В основном идёт

  подробнее ...

Рейтинг: 0 ( 0 за, 0 против).
iv4f3dorov про Тюрин: Цепной пес самодержавия (Альтернативная история)

Афтырь упоротый мудак, жертва перестройки.

Рейтинг: +1 ( 1 за, 0 против).
iv4f3dorov про Дорнбург: Змеелов в СССР (Альтернативная история)

Очередное антисоветское гавно размазанное тонким слоем по всем страницам. Афтырь ты мудак.

Рейтинг: +2 ( 3 за, 1 против).
A.Stern про Штерн: Анархопокалипсис (СИ) (Боевик)

Господи)))
Вы когда воруете чужие книги с АТ: https://author.today/work/234524, вы хотя бы жанр указывайте правильный и прологи не удаляйте.
(Заходите к автору оригинала в профиль, раз понравилось!)

Какое же это фентези, или это эпоха возрождения в постапокалиптическом мире? -)
(Спасибо неизвестному за пиар, советую ознакомиться с автором оригинала по ссылке)

Ещё раз спасибо за бесплатный пиар! Жаль вы не всё произведение публикуете х)

Рейтинг: 0 ( 1 за, 1 против).
чтун про серию Вселенная Вечности

Все четыре книги за пару дней "ушли". Но, строго любителям ЛитАниме (кароч, любителям фанфиков В0) ). Не подкачал, Антон Романович, с "чувством, толком, расстановкой" сделал. Осталось только проду ждать, да...

Рейтинг: +2 ( 2 за, 0 против).

Основы Redux [М. Пацианский] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]

Содержание
Вступление
От автора
Подготовка

1.1
1.1.1
1.2

create-react-app

1.2.1

ESLint и Prettier

1.2.2

Создание

1.3

Основы Redux (теория)

1.3.1

Точка входа

1.3.2

Редьюсеры и connect

1.3.3

Комбинирование редьюсеров

1.3.4

Контейнеры и компоненты

1.3.5

Создание actions

1.3.6

Константы

1.3.7

Наводим порядок

1.3.8

Middleware (усилители)

1.3.9

Асинхронные actions

1.3.10

Взаимодействуем с VK

1.3.11

Рефакторинг

1.3.12

Оптимизация перерисовок

1.3.12.1

Доработки

1.3.12.2

Что дальше?

1.4

Спасибо

1.5

1

Вступление

React Redux [RU tutorial] (2-е издание)
Версия от 2018 года включает в себя React
16.4.3

) и Redux

^16.4.1

(без проблем апгрейдится до

^4.0.0

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

В данном учебном курсе вы найдете 2 раздела:
1. Подготовка (сильно похудела во втором издании, так как появился CRA)
2. Теория redux и создание веб-приложения по шагам
Курс предполагает, что читатель уже знаком с React. Если вы не знакомы, рекомендую
для начала ознакомиться с курсом React.js для начинающих.
В результате прохождения курса, вы научитесь:
Основам создания SPA-приложения на React;
Грамотно готовить Redux-приложение (однонаправленный поток данных);
Выполнять асинхронные запросы (прелоадер, обработка ошибок) с помощью
стандартного redux-thunk;
Взаимодействовать со сторонними API (на примере VK API);
Работать с документацией (по-желанию);
Оптимизировать перерисовки компонентов;
Результатом изучения будет приложение, которое выведет ваши фото из VK
отсортированные по лайкам с фильтром по году.

2

Вступление

Поддержать проект
Вы можете поддержать проект, мне будет очень приятно.
Если у вас не получается поддержать проект материально, вы можете оставить отзыв
в группе vk.

3

От автора

Обо мне

Меня зовут Максим Пацианский, я Frontend-разработчик, стартанул в этой теме с
Joomla! сайтов в 2008 году.
Занимаюсь консультированием по React более 2х лет, с момента выхода прошлых
учебников.
Подробнее о моем опыте консультирования я писал на хабре.

Напутствие
Пожалуйста, выполняйте код по ходу книги. Ломайте его, "консольте", интересуйтесь.

Полезные ссылки
Мои уроки/вебинары/соц.сети:
Полноценный учебник "Основы React"
Расписание стримов и вебинаров (на сайте есть текстовые версии вебинаров)
Youtube канал c записями вебинаров и стримов
Группа vkontakte
Канал в telegram
Twitter
Facebook
React.js (EN) - офф.сайт, содержит примеры для изучения

4

От автора

Redux (EN) - документация по Redux (так же есть примеры)

Консультации и платные услуги
С 2016 года, я с удовольствием занимаюсь консультированием 1 на 1, поиском
проблем в коде, помощью в подготовке к собеседованию и т.д. Хороший багаж опыта,
которым я готов поделиться понятным языком.
Актуальный прайс

5

Подготовка

Подготовка
Данная глава является обучающей для людей, которые не в курсе, или хотят освежить
и пополнить свою базу знаний, по следующим пунктам:
Установка create-react-app (CRA) [копия разделов из учебника по основам React]
настройка VS Code для удобной работы
настройка Prettier
настройка ESLint
Результатом подготовки, будет следующий код.
Если вам понятен код данного раздела, предлагаю сразу переходить к части
"Создание".
Для всех остальных, я предлагаю за несколько простых шагов настроить удобное
рабочее окружение.

6

create-react-app

Установка и запуск create-react-app
npx create-react-app my-app
cd my-app
npm start

Если вы не знакомы с данными командами, значит вам нужно поставить себе node.js и
ввести их в терминале после.

После запуска мы получим следующую картину в браузере:

И следующую файловую структуру:
+-- node_modules (здесь расположены пакеты для работы приложения)
+-- public (здесь расположены публичные файлы, такие как index.html и favicon)
+-- src (здесь живет компонент App)
+-- .gitignore (файл для гита)
+-- package.json (файл с зависимостями проекта)
+-- README.md (описание проекта)
+-- yarn.lock (может быть, а может и не быть - тоже относится к теме зависимостей прое
кта)

7

create-react-app

CRA при каждом изменении в файлах внутри директории src - перезагружает страницу
в браузере.

Про import/export задерживаться не будем, так как думаю вы это уже знаете. Если что,
есть глава "Приборка и импорты" в учебнике по основам реакта.

Исходный код

8

ESLint и Prettier

ESLint и Prettier
Кратко о библиотеках:

ESLint
Линтер - это помощник по части "здоровья" кода. Вы определяете список правил и в
дальнейшем, при настроенном плагине в вашем редакторе, он как Microsoft Word
"проферка орфографии" проверяет все, что вы написали.
Например, определили переменную, но нигде не используете? Сработает правило: nounused-vars (долой неиспользуемые переменные) и переменная будет подчеркнута.

Когда вы видите "подчеркивание", и после наведения видите в скобочках название
правила - не нужно бежать гуглить. Нужно идти на сайт eslint.org и там в "поиск"
вставлять текст ошибки, будет быстрее.

9

ESLint и Prettier

Prettier
Преттир - это помощник по части оформления кода. Можно писать с пробелами перед
именем свойства, кавычками, запятыми в последней строке и тд тп - преттир,
настроенный на сохранение или на пре-коммит хук - "перетрясет" ваши файлы и
оформит их в соответствии с настройками, которых у него минимум. Это сделано
специально, ибо чем меньше настроек, тем меньше конфигураций - когда-нибудь, спор
"табы vs пробелы" уйдет в небытие, но кто выиграет?)
Одна из работ "преттира" - форматировать длинные строки.
Было:

10

ESLint и Prettier

Стало:

Я думаю преимущества очевидны, поэтому давайте настроим необходимые
ускорители повседневной разработки.

Настройка
Линтер встроен в create-react-app, но для работы в связке с Prettier, а так же для
подсветки кода во время написания в VS Code нужна небольшая донастройка.
Для начала установите пакеты:
npm install eslint-config-prettier eslint-plugin-prettier prettier lint-staged husky -save-dev

Все пакеты в целом понятны зачем, кроме lint-staged и husky

11

ESLint и Prettier

husky - упрощает работу с git hooks ("пре-коммит" (момент, когда вы собираетесь
делать коммит) легко настроить с помощью этой "собаки")
lint-staged - пакет, который позволяет вам сделать обработку командой из
терминала только тех файлов, которые собираются улететь в коммит.
Husky и lint-staged - сладкая парочка для борьбы с плохим кодом в нашем
репозитории. Например, мы можем настроить, что если ESLint вернул ошибку, то
коммит будет автоматически отменен. Вернемся к этому позже.
Итак, настройка eslint, создайте следующий файл в корне проекта:
.eslintrc
{
"extends": [
"react-app",
"prettier"
],
"rules": {
"jsx-quotes": [
1,
"prefer-double"
]
},
"plugins": [
"prettier"
]
}

Достаточно скромный конфиг, который "наследует" стандартные правила (их много) из
react-app и prettier (это глобальные конфиги, один встроен в create-react-app, второй
мы установили посредством пакета eslint-config-prettier)
Затем я переопределил одно правило: jsx-quotes (для имен классов внутри JSX будут
ставиться двойные кавычки. Не могу сказать, насколько это важно на сегодняшний
день, но раньше у меня были конфликты с преттиром без этого правила).
Вы можете переопределить в списке любые правила, которые вас интересуют. Список
можно найти в документации, но проще просто начать работать и по ходу пьесы
смотреть на "подчеркивания". Те, которые вас не устраивают - переопределяйте.
Последняя опция в конфиге - использование плагинов. Мы используем плагин prettier
(пакет eslint-plugin-prettier), чтобы не было конфликтов между "помощниками"
(напоминаю, у нас их два: prettier и eslint).
После настройки конфига, вам нужно настроить ваш редактор. Я приведу пример
только для Visual Studio Code.

12

ESLint и Prettier

Добавьте в файл с настройками, следующие строки:
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"[javascript]": {
"editor.formatOnSave": true,
},
"[html]": {
"editor.formatOnSave": false,
},
"[json]": {
"editor.formatOnSave": false,
},
"eslint.autoFixOnSave": true,
"eslint.alwaysShowStatus": true,

Напоследок, для корректной работы вам потребуется парочка плагинов из маркетплейса (eslint и prettier).
Мой список плагинов:

Конфиг может быть настроен различными способами, например, взгляните на эти два
видео:
Add ESLint & Prettier to VS Code for a Create React App
How to Setup VS Code + Prettier + ESLint

Настроим prettier (нам так же нужен конфигурационный файл):
.prettierrc

13

ESLint и Prettier

{
"useTabs": false, // использовать табы? нет (я за пробелы)
"printWidth": 80, // длина строки - 80
"tabWidth": 2, // длина "таба" - 2 пробела
"singleQuote": true, // использовать одинарные кавычки - да!
"trailingComma": "es5", // запятая в последней строке - да
"jsxBracketSameLine": false, // закрывающийся jsx в этой же строке
"parser": "flow", // парсер - flow (пока не важно)
"semi": false // точка с запятой - нет
}

Вот и все настройки. Настройка - parser, вам пока не должна мешать, а что такое
trailingComma - пример ниже:
const data = {
name: 'Max',
city: 'Moscow, // просто render
class App extends Component {
render() {
return (


Welcome to React


To get started, edit src/App.js and save to reload.


)
}
}
export default App

Выполните команду в терминале (находясь в директории с проектом):
node_modules/.bin/eslint src/

Так как я не люблю глобальные зависимости, я использую локально установленный
eslint (его установил для нас create-react-app). Чтобы упростить вызов в терминале,
можно добавить в секцию scripts в package.json новую команду:
package.json

16

ESLint и Prettier

...
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"precommit": "lint-staged",
"eslint": "node_modules/.bin/eslint src/"
},
...

Теперь eslint в терминале можно запускать так:

npm run eslint

. После запуска этой

команды, eslint проверит весь src/ на наличие ошибок/предупреждений. Это полезно
сделать в начале внедрения "жесткого пре-коммита" и лично исправить все ошибки,
чтобы команда научилась на хорошем примере.
Вернемся к настройке. Изменим lint-stage скрипт в package.json на:
"lint-staged": {
"*.{js, jsx}": [
"node_modules/.bin/eslint --max-warnings=0",
"prettier --write",
"git add"
]
}

Теперь в момент пре-коммита будет запускаться lint-staged проверка в которой eslint и
prettier обработают все файлы, готовящиеся к коммиту.
Что интересно, я настроил еслинт агрессивно (опция

--max-warnings=0

), то есть, даже

любое предупреждение прервет коммит.
Проверим:

17

ESLint и Prettier

На скрине видно "вредный совет". Да, если добавить
commit

--no-verify

к команде

git

, то проверок не будет. Но за это сразу бейте по рукам.

Итого: Настроили ESLint, prettier и pre-commit hook. Очень сильно облегчили жизнь
себе и коллегам, кто болеет за единый стиль и чистый код.
Исходный код (без ошибок).

18

ESLint и Prettier

19

Создание

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

Прежде чем описывать структуру, давайте в общих чертах взглянем на Redux.
Redux - приложение это:
состояние (store) приложения в одном месте;
однонаправленный поток данных: случился action -> редьюсер по команде "фас"
отработал и вернул новое состояние -> компонент(ы) обновились;
Redux вдохновлен Flux методологией и языком программирования Elm
Под капотом, Redux использует старую фичу реакта - context, которая обрела вторую
жизнь в версии реакта 16.3 - "New context API".
Есть старый context, который использует Redux, и есть новое Context API, не путайте.

Файлы и папки:
20

Создание

Изначально наше приложение в файловом менеджере должно выглядеть так
(создайте недостающие директории в src):
+-- src
|

+-- actions

|

+-- components

|

+-- containers

|

+-- reducers

|

+-- utils

+-- файлы-от-create-react-app
+-- ...

Для обучения мы будем использовать очень распространенный подход организации
файлов: деление на контейнеры и компоненты + экшены и редьюсеры в отдельных
директориях.
Есть и другие подходы, мне нравится композиция по фичам/страницам (EN).

21

Основы Redux (теория)

Основы Redux (теория)
Курс рассчитан на создание приложения по шагам, а это значит максимум практики и
минимум теории. Этот самый минимум, перед вами.
Давайте еще раз взглянем на схему нашего приложения:

В шапке слева заголовок и три кнопки выбора года. Ниже - фото соответствующего
года, отсортированное по количеству лайков.
В шапке справа - ссылка войти/выйти.
Представим, как должны выглядеть данные для такой страницы:
app: {
page: {
year: 2016,
photos: [photo, photo, photo...]
},
user: {
name: 'Имя',
...
}
}

22

Основы Redux (теория)

Поздравляю вас, мы только что описали как должно выглядеть состояние (state)
нашего приложения.
За содержание всего состояния нашего приложения, отвечает объект Store. Как уже не
раз упоминалось - это обычный объект

{}

. Важно, что в отличии от Flux, в Redux

только один объект Store.
Не хочется оставлять вас надолго без практики, поэтому процесс создания store и
немного подробностей про него я аккуратно вплету в следующие главы, а пока
достаточно того, что: store, "объединяет" редьюсер(ы) (reducer) и действия (actions), а
так же имеет несколько чрезвычайно полезных методов, например:
getState()

- позволяет получить состояние приложения;

dispatch(action)

- позволяет обновлять состояния, путем вызова ("диспатча")

действия;
subcribe(listener)

- регистрирует слушателей;

Actions
Actions описывают действия.
Actions - это объект. Обязательное поле - type. Так же, если вы хотите следовать
соглашению, все данные, которые передаются вместе с действием, кладите внутрь
свойства payload. Таким образом, для нашего приложения, мы можем составить,
например такую пару действий (actions):
{
type: 'ЗАГРУЗИ_ФОТО',
payload: 2018 //год
}

{
type: 'ФОТО_ЗАГРУЖЕНЫ_УСПЕШНО',
payload: [массив фото]
}

Чтобы вызвать actions, мы должны написать функцию, которая в рамках Flux/Redux
называется - ActionsCreator (создатель действия), но перед этим стоит принять во
внимание, что обычно тип действия, описывают как константу.
Например, константы нашего проекта:

23

Основы Redux (теория)

const GET_PHOTO_REQUEST = 'GET_PHOTO_REQUEST'
const GET_PHOTO_SUCCESS = 'GET_PHOTO_SUCCESS'

Не все любят данный подход с константами, но он был родоначальником, плюс его
легко объяснить. К тому же, я до сих пор сторонник этого подхода.
Вернемся, к ActionsCreator, один из наших "создателей действий", выглядел бы так:
function getPhotos(year) {
return {
type: GET_PHOTOS,
payload: year
}
}
// я буду использовать синтаксис function внутри actions, так как не вижу смысла
// в изменении его на такую запись:
const getPhotos = (year) => ({
type: GET_PHOTOS,
payload: year,
})

Итого: actions сообщает нашему приложению - "Эй, что-то произошло! И я знаю, что
именно!"

Reducer
"Actions описывает факт, что что-то произошло, но не указывает, как состояние
приложения должно измениться в ответ, это работа для Reducer'а" - (офф.
документация)
Наше приложение не нуждается в нескольких редьюсерах, но крайне необходимо
познакомить читателя с reducer composition, так как это фундаментальный шаблон
построения redux приложений: мы разбиваем наше глобальное состояние на
кусочки, за каждый кусочек отвечает свой reducer. Кусочки объединяются в
Корневом Редьюсере (rootReducer).
Для того, чтобы научиться комбинировать редьюсеры, мы добавим в приложение
reducer - user, который просто будет отображать имя, если пользователь залогинился.
Ниже на схеме - сноска [1].
Схематично, наше приложение можно представить так:

24

Основы Redux (теория)

Так как у нас есть reducer'ы

page

и

user

, можно представить следующий диалог:

pageActions: Пришло 123 фото
Reducer (page): Ок, нужно положить эти 123 фото в page.photos

А на js выглядело бы так:
function page(state = initialState, action) {
switch (action.type) {
case GET_PHOTO_SUCCESS:
return Object.assign({}, state, {
photos: action.payload
})
default:
return state
}
}

Обратите внимание, мы не мутировали наш state, мы создали новый state. Это важно.
Крайне важно. В редьюсере, мы всегда должны возвращать новый объект, а не
измененный предыдущий.
На практике, я буду использовать object spread syntax, поэтому предыдущую функцию
с Object.assign можно переписать следующим образом:

25

Основы Redux (теория)

function page(state = initialState, action) {
switch (action.type) {
case GET_PHOTO_SUCCESS:
return {...state, photos: action.payload} //Object spread syntax
default:
return state
}
}

Объект, который мы возвращаем в редьюсере, далее с помощью функции

connect

,

превратится в свойства для компонентов. Таким образом, если продолжить пример с
фото, то можно написать такой псевдо-код:


Благодаря этому, внутри компонента



, мы сможем получить фото, как

this.props.photos

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

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

26

Точка входа

Точка входа
(Вы можете взять ветку из репозитория, который мы создали в процессе
настройки для старта выполнения урока. Практика очень важна.)
Подтянем Redux и react-redux в наш проект:
npm i redux react-redux --save

Точка входа в наше приложение - src/index.js
Обновим его содержимое:
src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './App'
import registerServiceWorker from './registerServiceWorker'
import './index.css'
const store = createStore(() => {}, {}) // [1]
ReactDOM.render(


,
document.getElementById('root')
)
registerServiceWorker()

Итак, первый компонент из мира Redux -



( [EN] документация ).

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

connect

, речь о

которой пойдет далее. Сейчас нам и получать нечего, так как store у нас - пустой
объект.
Давайте подробнее посмотрим на строку [1]:

27

Точка входа

const store = createStore( () => {}, {})

Во-первых, если вам трудно читать ES2015 код, то переводите его в привычный ES5, с
помощью babel-playground.
На скриншоте ниже: слева - современный код, справа - старый ES5 код, после
преобразования.

Во-вторых, давайте взглянем на документацию метода createStore: принимает один
обязательный аргумент (функцию reducer) и парочку не обязательных (начальное
состояние и "усилители").
Теперь переведем то, что мы написали, когда присваивали store:
Возьми пустую анонимную функцию в качестве редьюсера и пустой объект в
качестве начального состояния. Если коротко: возьми ничего и "ничего" не делай.
Предлагаю вынести создание store в отдельный файл, так как в нем мы добавим позже
несколько строк кода, в том числе, добавим усилителей (enhancers).
src/store/configureStore.js
import { createStore } from 'redux'
export const store = createStore(() => {}, {})

Поправить импорт в индексе:
src/index.js

28

Точка входа

import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { store } from './store/configureStore' // исправлено
import App from './App'
import registerServiceWorker from './registerServiceWorker'
import './index.css'
ReactDOM.render(


,
document.getElementById('root')
)
registerServiceWorker()

Усилители - это middleware функции. Если читатель знаком с express.js, то он знаком с
усилителями в redux. Для остальных: типичный усилитель - логгер (logger), который
просто пишет в консоль все что происходит с наблюдаемым объектом.

Давайте так же исправим App.js, чтобы обозначить чем мы тут с вами занимаемся:
import React, { Component } from 'react'
import './App.css'
class App extends Component {
render() {
return (


Мой топ фото

Здесь будут мои самые залайканые фото

)
}
}
export default App

29

Точка входа

Итого: мы настроили точку входа для redux-приложения (src/index.js), в которой
обернули все в



. Так же вынесли для будущего удобства настройку store в

отдельный файл.
Исходный код.

30

Редьюсеры и connect

Создание Reducer
Создадим "корневой редьюсер" (rootReducer).
src/reducers/index.js
export const initialState = {
user: 'Unknown User',
}
export function rootReducer(state = initialState) {
return state
}

В этой функции нечего комментировать. Просто возвращается

{user: 'Unknown User'}

(неизвестный пользователь).
В дальнейшем мы будем комбинировать редьюсеры в корневом редьюсере, но сейчас
нам важно отобразить имя юзера (Unknown User) в компоненте, чтобы вы не заскучали
от чтения.
Главное, что нужно сейчас держать в голове: корневой редьюсер - это и есть
представление всего нашего состояния приложения (то есть, всего нашего store).
Сконфигурируем store:
src/store/configureStore.js
import { createStore } from 'redux'
import { rootReducer, initialState } from '../reducers'
export const store = createStore(rootReducer, initialState)

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

Связывание данных из store с
компонентами приложения
31

Редьюсеры и connect

В разделе Точка входа шла речь о некой функции connect, которая поможет нам
получить в качестве props для компонента



данные из store. Добавим ее:

src/App.js
import React, { Component } from 'react'
import { connect } from 'react-redux'
import './App.css'
class App extends Component {
render() {
return (


Мой топ фото

Здесь будут мои самые залайканые фото
Меня зовут: {this.props.user} {/* добавлен вывод из props */}

)
}
}
// приклеиваем данные из store
const mapStateToProps = store => {
console.log(store) // посмотрим, что же у нас в store?
return {
user: store.user,
}
}
// в наш компонент App, с помощью connect(mapStateToProps)
export default connect(mapStateToProps)(App)

Назначение функции connect вытекает из названия: подключи React компонент к
Redux store.
Результат работы функции connect - новый присоединенный компонент, который
оборачивает переданный компонент.
У нас был компонент



, а на выходе получился



. В этом не

трудно убедиться, если взглянуть в react dev tools.

32

Редьюсеры и connect

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



теперь есть метод redux store - dispatch, и объект свойств (в

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

Давайте еще поиграемся с простым примером. Для начала изменим набор данных:
src/reducers/index.js
export const initialState = {
user: { // мы вложили в user вместо строки, объект
name: 'Василий',
surname: 'Реактов',
age: 27,
},
}
export function rootReducer(state = initialState) {
return state
}

затем подкрутим компонент:
src/containers/App.js

33

Редьюсеры и connect

// ... (импорты)
class App extends Component {
render() {
const { name, surname, age } = this.props.user
return (


Мой топ фото


Привет из App, {name} {surname}!

Тебе уже {age} ?

)
}
}
// ...

(mapStateToProps и connect - не изменились)

Все работает ровно так, как мы указали: в объект user "подключилось" все состояние
нашего приложения, которое сейчас очень простое и описано в src/reducer/index.js.

34

Редьюсеры и connect

Итого: мы научились "вытаскивать" данные из стора в компонент, с помощью

connect

.

Исходный код на текущий момент.

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

Полезные ссылки:
connect (офф.документация)

35

Комбинирование редьюсеров

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

из пакета

redux

собрать их воедино. Причем, абсолютно никакой

магии, combineReducers просто возвращает "составной" редьюсер.
Для нашего приложения, можно выделить следующие reducer'ы (согласно схеме из
предыдущих глав):
user
page
Создадим их:
src/reducers/user.js
const initialState = {
name: 'Аноним',
}
export function userReducer(state = initialState) {
return state
}

src/reducers/page.js
const initialState = {
year: 2018,
photos: [],
}
export function pageReducer(state = initialState) {
return state
}

Обновим точку входа для редьюсеров:
src/reducers/index.js

36

Комбинирование редьюсеров

import { combineReducers } from 'redux'
import { pageReducer } from './page'
import { userReducer } from './user'
export const rootReducer = combineReducers({
page: pageReducer,
user: userReducer,
})

Обновим configureStore:
src/store/configureStore.js
import { createStore } from 'redux'
import { rootReducer } from '../reducers'
// удалили "начальное состояние = initial state"
// так как теперь наш редьюсер составной,
// и нам нужны initialState каждого редьюсера.
// Это будет сделано автоматически.
export const store = createStore(rootReducer)

Посмотрим что у нас теперь "консолится" в компоненте



, а так же в React dev

tools.

37

Комбинирование редьюсеров

Сейчас в браузере у нас нерабочее приложение. В чем же проблема?
Ответ кроется в работе функции connect и в функции mapStateToProps из нашего
файла App.js. Сейчас у нас там написано следующее:
const mapStateToProps = store => {
console.log(store)
return {
user: store.user,
}
}

Что можно перевести так: возьми полностью "стор" приложения и присоедини его в
переменную user, дабы она была доступна из компонета App.js как

this.props.user

38

Комбинирование редьюсеров

Здесь, я предложу простую задачку на понимание происходящего. Измените
компонент App.js и функцию mapStateToProps так, чтобы получить следующую
картину:

Ответ:
src/containers/App.js
...
class App extends Component {
render() {
const { user, page } = this.props
return (


Мой топ фото

Привет, {user.name}!

У тебя {page.photos.length} фото за {page.year} год


)
}
}
const mapStateToProps = store => {
console.log(store)
return {
user: store.user,
page: store.page,
}
}
...

39

Комбинирование редьюсеров

Работа функции mapStateToProps многих вводит в ступор. В данной функции, мы
хотим отрезать от нашего общего пирога (Store) только те кусочки (редьюсеры),
которые нам нужны.
Еще можно применить аналогию: мы приклеиваем в props компонента, данные из тех
редьюсеров, которые нам требуются.
А если быть более точным, то мы не только получаем в this.props.XXX данные,
которым нам нужны, но мы еще и подписываемся на изменение этих данных.
После того, как вы знаете о подписке, пора вам раскрыть еще один козырь - когда мы
подписываемся только на нужные редьюсеры в компоненте, перерисовка происходит
только в случае изменения конкретно этих данных. Если же мы бы подписались просто
на весь корневой редьюсер, то не важно в каком бы редьюсере изменились данные все подписанные на корневой редьюсер компоненты обновились бы.
Опять же, в теории это все абсолютно не "зайдет" не подготовленному читателю.
Поэтому на практике мы еще не раз разберем данную информацию.

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

Полезные ссылки:
combineReducers (офф.документация)

40

Контейнеры и компоненты

Контейнеры и компоненты
Прежде чем мы разобьем App.js на компоненты



и



хотелось бы

отметить про способ разделения на "компоненты" и "контейнеры", иначе называемый:
деление на "глупые" и "умные" компоненты, "Presentational" и "Container" и быть может
как-то еще.
Позволю себе в очередной раз прибегнуть к офф.документации и перевести таблицу
различий, которая отлично и кратко отражает суть.

Цель
Осведомлен о
Redux
Для
считывания
данных
Для
изменения
данных
Пишутся

Компонент (глупый)

Контейнер (умный)

Как это выглядит
(разметка, стили)

Как это работает (получение данных,
обновление состояния)

Нет

Да

Читает данные из
props

Подписан на Redux state (состояние)

Вызывает callback из
props

Отправляет (dispatch) Redux действие
(actions)

Вручную

Обычно, генерируются Redux

Магия таблиц обычно проявляется не сразу. Если переписать наше приложение, а
потом взглянуть сюда еще раз - многое станет гораздо яснее. Предлагаю так и
поступить. Поехали!
Установим prop-types и создадим компоненты.
npm install --save prop-types

src/components/User.js

41

Контейнеры и компоненты

import React from 'react'
import PropTypes from 'prop-types'
export class User extends React.Component {
render() {
const { name } = this.props
return (

Привет, {name}!

)
}
}
User.propTypes = {
name: PropTypes.string.isRequired,
}

src/components/Page.js
import React from 'react'
import PropTypes from 'prop-types'
export class Page extends React.Component {
render() {
const { year, photos } = this.props
return (


У тебя {photos.length} фото за {year} год


)
}
}
Page.propTypes = {
year: PropTypes.number.isRequired,
photos: PropTypes.array.isRequired,
}

Наш файл App.js - это контейнер (так как подключен к redux). Изменим-с...
src/containers/App.js

42

Контейнеры и компоненты

import React, { Component } from 'react'
import { connect } from 'react-redux'
import { User } from '../components/User'
import { Page } from '../components/Page'
import './App.css'
class App extends Component {
render() {
const { user, page } = this.props
return (


Мой топ фото




)
}
}
const mapStateToProps = store => {
return {
user: store.user,
page: store.page,
}
}
export default connect(mapStateToProps)(App)

Не забудьте так же перенести App.css в src/containers и поменять подключение
/>

next => action => {
console.log('ping')
return next(action)
}
/*eslint-enable */

59

Middleware (усилители)

Боюсь, здесь не обойтись без ES5 версии:
var ping = function ping(store) {
return function (next) {
return function (action) {
console.log('ping');
return next(action);
};
};
};

Поехали:
eslint-disable - просто выключает проверку этого блока "линтером".
ping - это функция, которая возвращает функцию. Middleware - это всегда функция,
которые обычно возвращают функцию, если только целью middleware не является
прервать цепочку вызовов.
в функциях, у нас становятся доступными аргументы, которые мы можем
использовать во благо приложения:
- redux-store нашего приложения;

store
next

- функция-обертка, которая позволяет продолжить выполнение цепочки;

action

- действие, которое было вызвано (как вы помните, вызванные

действия - это store.dispatch)

Сейчас, при клике на кнопки, у нас в консоли появляется строка ping. Давайте изменим
ее, написав простейший логгер:
store/enhancers/ping.js

60

Middleware (усилители)

/*eslint-disable */
export const ping = store => next => action => {
console.log(
`Тип события: ${action.type}, дополнительные данные события: ${
action.payload
}`
)
return next(action)
}
/*eslint-enable */

Я использовал новый строковый синтаксис. В прошлом, наш console.log выглядел бы
так:
console.log('Тип события: ' + action.type + ', дополнительные данные события: ' + acti
on.payload)

Покликайте на кнопки, результат должен быть следующим:

Redux-logger
Отбросим наш велосипед и поставим популярный логгер.
npm i --save-dev redux-logger

Удалите папку enchancers, и измените configureStore.
src/store/configureStore.js

61

Middleware (усилители)

import { createStore, applyMiddleware } from 'redux'
import { rootReducer } from '../reducers'
import logger from 'redux-logger'
export const store = createStore(rootReducer, applyMiddleware(logger))

Можете проверить - логгер достаточно информативный и удобен в использовании.

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

62

Асинхронные actions

Асинхронные actions
Давайте представим синхронное действие:
Пользователь кликнул на кнопку
dispatch action

{type: ТИП_ДЕЙСТВИЯ, payload: доп.данные}

интерфейс обновился
Давайте представим асинхронное действие:
Пользователь кликнул на кнопку
dispatch action

{type: ТИП_ДЕЙСТВИЯ_ЗАПРОС}

запрос выполнился успешно
dispatch action

{type: ТИП_ДЕЙСТВИЯ_УСПЕШНО, payload: доп.данные}

запрос выполнился неудачно
dispatch action

{type: ТИП_ДЕЙСТВИЯ_НЕУДАЧНО, error: true, payload: доп.данные

ошибки}

Благодаря такой схеме, в reducer'e мы сможем реализовать подобное:
switch(тип_действия)
case ТИП_ДЕЙСТВИЯ_ЗАПРОС:
покажи preloader
case ТИП_ДЕЙСТВИЯ_УСПЕШНО:
скрой preloader, покажи данные
case ТИП_ДЕЙСТВИЯ_НЕУДАЧНО:
скрой preloader, покажи ошибку

Как нам известно, действие - это простой объект, который возвращается функцией его
создающей (action creator).
Убедимся в этом:
src/actions/PageActions.js
export const SET_YEAR = 'SET_YEAR'
export function setYear(year) {
return {
type: SET_YEAR,
payload: year,
}
}

63

Асинхронные actions

Было бы неплохо иметь возможность возвращать не простой объект, а функцию,
внутри которой иметь доступ к методу

dispatch

, чтобы можно было диспатчить

события в момент, когда они совершились. Псевдокод, мог бы выглядеть так:
export function getPhotos(year) {
return (dispatch) => {
dispatch({
type: GET_PHOTOS_REQUEST
})
$.ajax(url)
.success(
dispatch({
type: GET_PHOTOS_SUCCESS,
payload: response.photos
})
)
.error(
dispatch({
type: GET_PHOTOS_FAILURE,
payload: response.error,
error: true
})
)
}
}

Но вот незадача, actions - это простой объект, и если action creator возвращает не
простой объект, а функцию, то это как-то... Подождите! Ведь это именно то, что нам
нужно: Если action creator возвращает не простой объект, а функцию - выполни ее,
иначе если это простой объект ... тадам, передай дальше. Более того, мы знаем, что
в цепочке middleware у нас как раз есть доступный метод dispatch! И еще бонусом
getState.
Отлично, мы только что поняли, что нам нужен еще один усилитель. Такой усилитель
уже написан, причем код его невероятно прост, я даже приведу его здесь:
усилитель: redux-thunk

64

Асинхронные actions

function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;

Нам остается лишь добавить зависимость в наш проект.
npm install redux-thunk --save

И добавить redux-thunk в цепочку усилителей перед логгером, так как логгер должен
быть последним усилителем в цепочке.
import { createStore, applyMiddleware } from 'redux'
import { rootReducer } from '../reducers'
import logger from 'redux-logger'
import thunk from 'redux-thunk'
export const store = createStore(rootReducer, applyMiddleware(thunk, logger))

Для практики, предлагаю написать следующее:
по клику на кнопку с номером года
меняется год в заголовке
ниже (где должны быть фото), появляется текст "Загрузка..."
после удачной загрузки*
убрать текст "Загрузка..."
отобразить строку "У тебя ХХ фото" (зависит, от длины массива, переданного
в action.payload)
* вместо реального метода загрузки, будем использовать setTimeout, который
является удобным для тренировок исполнения асинхронных запросов.
Вы можете попробовать выполнить это задание сами, а потом сравнить его с
решением ниже.
65

Асинхронные actions

Для отображения / скрытия фразы "Загрузка...", используйте в reducer'е еще одно
свойство у состояния. Например, isFetching:
const initialState = {
year: 2016,
photos: [],
isFetching: false
}

Решение ниже.

Изменим action creator: src/actions/PageActions.js
export const GET_PHOTOS_REQUEST = 'GET_PHOTOS_REQUEST'
export const GET_PHOTOS_SUCCESS = 'GET_PHOTOS_SUCCESS'
export function getPhotos(year) {
return dispatch => {
// экшен с типом REQUEST (запрос начался)
// диспатчится сразу, как будто-бы перед реальным запросом
dispatch({
type: GET_PHOTOS_REQUEST,
payload: year,
})
// а экшен внутри setTimeout
// диспатчится через секунду
// как будто-бы в это время
// наши данные загружались из сети
setTimeout(() => {
dispatch({
type: GET_PHOTOS_SUCCESS,
payload: [1, 2, 3, 4, 5],
})
}, 1000)
}
}

Изменим reducer: src/reducers/page.js

66

Асинхронные actions

import { GET_PHOTOS_REQUEST, GET_PHOTOS_SUCCESS } from '../actions/PageActions'
const initialState = {
year: 2018,
photos: [],
isFetching: false, // изначально статус загрузки - ложь
// так как он станет true, когда запрос начнет выполнение
}
export function pageReducer(state = initialState, action) {
switch (action.type) {
case GET_PHOTOS_REQUEST:
return { ...state, year: action.payload, isFetching: true }
case GET_PHOTOS_SUCCESS:
return { ...state, photos: action.payload, isFetching: false }
default:
return state
}
}

У нас готова логика для обновления состояния (и интерфейса, разумеется). Осталось
поправить отображение.
Так как мы переписали и переименовали функцию (setYear -> getPhotos):
src/containers/App.js

67

Асинхронные actions

import React, { Component } from 'react'
import { connect } from 'react-redux'
import { User } from '../components/User'
import { Page } from '../components/Page'
import { getPhotos } from '../actions/PageActions'
class App extends Component {
render() {
const { user, page, getPhotosAction } = this.props
return (




)
}
}
const mapStateToProps = store => {
return {
user: store.user,
page: store.page,
}
}
const mapDispatchToProps = dispatch => {
return {
getPhotosAction: year => dispatch(getPhotos(year)),
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(App)

Обновим соответствующий компонент:
src/components/Page.js

68

Асинхронные actions

import React from 'react'
import PropTypes from 'prop-types'
export class Page extends React.Component {
onBtnClick = e => {
const year = +e.currentTarget.innerText
this.props.getPhotos(year) // setYear -> getPhotos
}
render() {
const { year, photos, isFetching } = this.props // вытащили isFetching
return (



2018
{' '}

2017
{' '}

2016
{' '}

2015
{' '}

2014


{year} год
{/* добавили отрисовку по условию */}
{isFetching ? Загрузка... : У тебя {photos.length} фото.}

)
}
}
Page.propTypes = {
year: PropTypes.number.isRequired,
photos: PropTypes.array.isRequired,
getPhotos: PropTypes.func.isRequired, // setYear -> getPhotos
// добавили новое свойство - isFetching, причем в propTypes нет boolean, есть bool
isFetching: PropTypes.bool.isRequired,
}

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

69

Асинхронные actions

Пока мы писали код для асинхронного запроса, мы НЕ нарушили главные принципы
redux-приложения:
1. Мы всегда возвращали новое состояние (новый объект, смотрите
src/reducers/page.js)
2. Мы строго следовали однонаправленному потоку данных в приложении: юзер
кликнул - возникло действие - редьюсер изменил - компонент отобразил.
Итого: вы можете сами дописать наше приложение, чтобы оно взаимодействовало с
VK, так как все что нужно, это добавить реальный асинхронный запрос (точнее парочку
- для логина, и для получения фото). Для этого придется почитать документацию по
работе с VK API.
Для тех, кто хочет добить пример поскорее - следующая глава, в которой мы загрузим
таки реальные фото из вашего профиля VK.
Исходный код на данный момент.

70

Взаимодействуем с VK

Взаимодействуем с VK
Чтобы работать с VK API вам необходимо будет создать приложение на сайте vk.com,
и указать в настройках URL сервера, с которого вы будете выполнять запросы.
По адресу https://vk.com/apps?act=manage создайте новое приложение (веб-сайт) и
заполните поля как на скриншоте, если используете локалхост и порт 3000.

Интеграция VK API
Необходимо добавить скрипт openapi (документация), а так же вызвать VK.init

71

Взаимодействуем с VK









Redux [RU] Tutorial v2



You need to enable JavaScript to run this app.




VK.init({
apiId: XXXXXX
});




Номер приложения можно посмотреть здесь:

Авторизация
Создадим действия для User.

72

Взаимодействуем с VK

src/actions/UserActions.js
export const LOGIN_REQUEST = 'LOGIN_REQUEST'
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'
export const LOGIN_FAIL = 'LOGIN_FAIL'
export function handleLogin() {
return function(dispatch) {
dispatch({
type: LOGIN_REQUEST,
})
//eslint-disable-next-line no-undef
VK.Auth.login(r => {
if (r.session) {
let username = r.session.user.first_name
dispatch({
type: LOGIN_SUCCESS,
payload: username,
})
} else {
dispatch({
type: LOGIN_FAIL,
error: true,
payload: new Error('Ошибка авторизации'),
})
}
}, 4) // запрос прав на доступ к photo
}
}

Так как загрузка информации из профиля - действие асинхронное, мы использовали
проверенную схему из трех действий:
XXX_REQUEST - диспатчим непосредственно перед стартом реального запроса
(для юзера это выглядит, как будто во время запроса)
XXX_SUCCESS + данные - если все прошло успешно добавляем данные [1]
ХХХ_FAIL + ошибка - если что-то пошло не так
[1] Чтобы достать имя пользователя, мы вытащили его из response(r).session. Данные
нам предоставил VK, так как мы подтвердили "разрешаю доступ" во всплывающем
окне.

73

Взаимодействуем с VK

"Приконнектим" в



UserActions, и добавим новые свойства в компонент



src/containers/App.js
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { User } from '../components/User'
import { Page } from '../components/Page'
import { getPhotos } from '../actions/PageActions'
import { handleLogin } from '../actions/UserActions'
class App extends Component {
render() {
// вытащили handleLoginAction из this.props
const { user, page, getPhotosAction, handleLoginAction } = this.props
return (



74

Взаимодействуем с VK

{/* добавили новые props для User */}


)
}
}
const mapStateToProps = store => {
return {
user: store.user, // вытащили из стора (из редьюсера user все в переменную thid.pr
ops.user)
page: store.page,
}
}
const mapDispatchToProps = dispatch => {
return {
getPhotosAction: year => dispatch(getPhotos(year)),
// "приклеили" в this.props.handleLoginAction функцию, которая умеет диспатчить ha
ndleLogin
handleLoginAction: () => dispatch(handleLogin()),
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(App)

Здесь мы поступили так же, как когда-то для page:
подписались на кусочек стора (user)
добавили экшен и передали его в dispatch в функции handleLoginAction
кусочек стора (user) и handleLoginAction - стали доступны нам в this.props
в



передали необходимые свойства

Обновим reducer user:
src/reducers/user.js

75

Взаимодействуем с VK

import { LOGIN_REQUEST, LOGIN_SUCCESS, LOGIN_FAIL } from '../actions/UserActions'
const initialState = {
name: '',
error: '', // добавили для сохранения текста ошибки
isFetching: false, // добавили для реакции на статус "загружаю" или нет
}
export function userReducer(state = initialState, action) {
switch (action.type) {
case LOGIN_REQUEST:
return { ...state, isFetching: true, error: '' }
case LOGIN_SUCCESS:
return { ...state, isFetching: false, name: action.payload }
case LOGIN_FAIL:
return { ...state, isFetching: false, error: action.payload.message }
default:
return state
}
}

В редьюсере есть интересные моменты:
когда мы начали делать запрос (LOGIN_REQUEST) мы очищаем error. Например,
была ошибка, мы стали делать новый запрос - ошибка очистилась;
если случился LOGIN_SUCCESS - мы в name записываем action.payload (а как вы
помните, там мы передаем в строке имя пользователя) и ставим статус загрузки false (то есть, не загружается, ибо загрузилось);
если случился LOGIN_FAIL - опять же, загружаю? Нет, значит isFetching - false.
Ошибка? Да - запиши в поле error.

Прокачаем



:

src/components/User.js

76

Взаимодействуем с VK

import React from 'react'
import PropTypes from 'prop-types'
export class User extends React.Component {
renderTemplate = () => {
const { name, error, isFetching } = this.props
if (error) {
return Во время запроса произошла ошибка, обновите страницу
}
if (isFetching) {
return Загружаю...
}
if (name) {
return Привет, {name}!
} else {
return (

Войти

)
}
}
render() {
return {this.renderTemplate()}
}
}
User.propTypes = {
name: PropTypes.string.isRequired,
error: PropTypes.string,
isFetching: PropTypes.bool.isRequired,
handleLogin: PropTypes.func.isRequired,
}

В коде компонента



ничего необычного нет. Рендерим шаблончик (в

зависимости от props).

Сейчас если кликнуть на "войти" - всплывет VK окно с подтверждением прав доступа
(первый раз). После подтверждения прав, вместо кнопки войти появляется надпись
"Привет, ХХХ". При перезагрузке сайта и повторных нажатиях на "войти" - VK окно
мгновенно закрывается, а кнопка вновь изменяется на "Привет, XXX".
Как всегда, доблестный логгер пишет в консоли - что происходит.

77

Взаимодействуем с VK

Загрузка фото
Нам нужно практически повторить, все что написано выше, только для блока Page.
Поэтому, наконец-то появилась самостоятельная задача. Я крайне рекомендую с ней
посидеть, так как это практически конец основного материала. Если у вас что-то не
получится - вы поймете что нужно закрепить, чтоперечитать. Не торопитесь смотреть
ответ, попробуйте сделать это сами, таким образом вы получите от этого учебника
гораздо больше. Да и кайфово это :)
Задача: используя метод photos.getAll вытащите свои фотографии из VK за год,
выбранный кнопкой. Отсортируйте их в обратном порядке по лайкам, чтобы самая
популярная фото оказалась первой.
После скриншотов есть подсказка: функция, которая делает запрос за фото.
Должно выглядеть следующим образом:

78

Взаимодействуем с VK

Подсказка: функция для загрузки фото

79

Взаимодействуем с VK

let photosArr = []
let cached = false
function makeYearPhotos(photos, selectedYear) {
let createdYear,
yearPhotos = []
photos.forEach(item => {
createdYear = new Date(item.date * 1000).getFullYear()
if (createdYear === selectedYear) {
yearPhotos.push(item)
}
})
yearPhotos.sort((a, b) => b.likes.count - a.likes.count)
return yearPhotos
}
function getMorePhotos(offset, count, year, dispatch) {
//eslint-disable-next-line no-undef
VK.Api.call(
'photos.getAll',
{ extended: 1, count: count, offset: offset, v: '5.80' },
r => {
try {
photosArr = photosArr.concat(r.response.items)
if (offset {
createdYear = new Date(item.date * 1000).getFullYear()
if (createdYear === selectedYear) {
yearPhotos.push(item)
}
})
yearPhotos.sort((a, b) => b.likes.count - a.likes.count)
return yearPhotos
}
function getMorePhotos(offset, count, year, dispatch) {
//eslint-disable-next-line no-undef
VK.Api.call(
'photos.getAll',
{ extended: 1, count: count, offset: offset, v: '5.80' },
r => {
try {

81

Взаимодействуем с VK

photosArr = photosArr.concat(r.response.items)
if (offset {
dispatch({
type: GET_PHOTOS_REQUEST,
payload: year,
})
if (cached) {
let photos = makeYearPhotos(photosArr, year)
dispatch({
type: GET_PHOTOS_SUCCESS,
payload: photos,
})
} else {
getMorePhotos(0, 200, year, dispatch)
}
}
}

makeYearPhotos

и

getMorePhotos

можно вынести в папку utils, как вспомогательные

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

82

Взаимодействуем с VK

Исправив редьюсер и отрисовку в компоненте, мы закончим начатое.
src/reducers/page.js
import {
GET_PHOTOS_REQUEST,
GET_PHOTOS_SUCCESS,
GET_PHOTOS_FAIL,
} from '../actions/PageActions'
const initialState = {
year: 2018,
photos: [],
isFetching: false,
error: '',
}
export function pageReducer(state = initialState, action) {
switch (action.type) {
case GET_PHOTOS_REQUEST:
return { ...state, year: action.payload, isFetching: true, error: '' }
case GET_PHOTOS_SUCCESS:
return { ...state, photos: action.payload, isFetching: false, error: '' }
case GET_PHOTOS_FAIL:
return { ...state, error: action.payload.message, isFetching: false }
default:
return state
}
}

src/components/Page.js
import React from 'react'
import PropTypes from 'prop-types'
export class Page extends React.Component {
onBtnClick = e => {
const year = +e.currentTarget.innerText
this.props.getPhotos(year) // setYear -> getPhotos
}
renderTemplate = () => {
const { photos, isFetching, error } = this.props
if (error) {
return Во время загрузки фото произошла ошибка
}
if (isFetching) {

83

Взаимодействуем с VK

return Загрузка...
} else {
return photos.map((entry, index) => ( // [1]




{entry.likes.count} ❤

))
}
}
render() {
const { year, photos } = this.props
return (



2018
{' '}

2017
{' '}

2016
{' '}

2015
{' '}

2014



{year} год [{photos.length}]

{this.renderTemplate()}

)
}
}
Page.propTypes = {
year: PropTypes.number.isRequired,
photos: PropTypes.array.isRequired,
getPhotos: PropTypes.func.isRequired,
error: PropTypes.string,
isFetching: PropTypes.bool.isRequired,
}

84

Взаимодействуем с VK

[1] - как вы заметили, мы использовали index в качестве ключа для наших div'ов.
Запустите пример, попробуйте поменять года. Возможно, вы словите баг, когда у
элементов с одинаковым индексом изображение меняется с задержкой. Проблема в
том, что мы использовали индекс для элементов, которые изменяются (а индекс-то
остается прежним! Ключ в итоге не изменяется, итого реакт "путается").
Чтобы этого избежать, сделайте ключ уникальным (например, для этого у нас есть id в
ответе от VK API):
if (isFetching) {
return Загрузка...
} else {
return photos.map(entry => (




{entry.likes.count} ❤

))
}

Теперь наш ключ (

key = {entry.id}

) уникальный и бага нет.

Мини-задачка на внимательность: если сейчас сгенерировать ошибку, то ничего не
отобразиться. Как это исправить?
Чтобы проверить ошибку, сделайте в функции запроса фото, поставьте

count: -1

:

src/actions/PageActions.js
...
function getMorePhotos(offset, count, year, dispatch) {
//eslint-disable-next-line no-undef
VK.Api.call(
'photos.getAll',
{ extended: 1, count: -1, offset: offset, v: '5.80' },
r => {
...

Проблема:

85

Взаимодействуем с VK

Решение:
...
class App extends Component {
render() {
const { user, page, getPhotosAction, handleLoginAction } = this.props
return (

{/* добавили error prop для Page */}

...
}
...

86

Взаимодействуем с VK

Итого: закрепили работу с асинхронными запросами.
Исходный код на текущий момент.
P.S. css тоже был слегка подправлен.

87

Рефакторинг

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



);

в action улетает текст с кнопки, если текст изменится - код сломается.
Проблема: большая связанность. Нужно облегчить.
возможно существует более простой путь "достать" из вк фото за конкретный год
(не рассматриваю это как проблему);
фраза "Привет, ИМЯ" после обновления страницы заменяется кнопкой "войти", то
есть не отображает реальной картины (фотографии у нас при этом доступны для
загрузки, то есть мы уже авторизованы);
после авторизации (или после перезагрузки) было бы неплохо сразу загружать
фото для 2018 года, так как юзер видит пустой экран и заголовок 2018;
Можно отнести это к "доработкам". Однако у нас есть место, которое является
опасным и о котором я лишь вскользь говорил в учебнике, пора исправится.
Приглашаю вас "убить" главную проблему текущего приложения - лишние
перерисовки компонента в следующем подразделе.
Остальные проблемы и будущие доработки живут в одноименном разделе.

88

Оптимизация перерисовок

Главная проблема приложения
У нас есть 2 компонента



и



. Мы специально сделали для них два

редьюсера, чтобы обновлять их независимо! А у нас? А у нас
обновляется при обновлении



Добавьте console.log в render метод у



каждый раз

и наоборот.


:

src/components/User.js
...
render() {
console.log(' render')
return {this.renderTemplate()}
}
...

89

Оптимизация перерисовок

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


становится тупым компонентом, который рендерит 2 контейнера:




Данные контейнеры - просто обертки над нашими компонентами, в которых мы
"подключаемся (connect) к Redux".
Так же, я сразу заменю название у экшенов внутри mapDispatchToProps: уберу оттуда
частичку Action.
В остальном, мы просто "разносим" то, что было в



по раздельным

контейнерам.
src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { store } from './store/configureStore'
import App from './components/App' // изменили путь
import registerServiceWorker from './registerServiceWorker'
import './index.css'
ReactDOM.render(


,
document.getElementById('root')
)
registerServiceWorker()

src/components/App.js

90

Оптимизация перерисовок

import React, { Component } from 'react'
import UserContainer from '../containers/UserContainer' // изменили импорт
import PageContainer from '../containers/PageContainer' // изменили импорт
class App extends Component {
render() {
return (




)
}
}
export default App

src/containers/PageContainer.js

91

Оптимизация перерисовок

import React from 'react'
import { connect } from 'react-redux'
import { Page } from '../components/Page'
import { getPhotos } from '../actions/PageActions'
class PageContainer extends React.Component {
render() {
const { page, getPhotos } = this.props
return (

)
}
}
const mapStateToProps = store => {
return {
page: store.page,
}
}
const mapDispatchToProps = dispatch => {
return {
getPhotos: year => dispatch(getPhotos(year)),
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(PageContainer)`

Как вы могли заметить, все что касалось



хранится в отдельном контейнере:

подписка на часть стора, экшен, пропсы...
То же самое, делаем для



src/containers/UserContainer.js

92

Оптимизация перерисовок

import React from 'react'
import { connect } from 'react-redux'
import { User } from '../components/User'
import { handleLogin } from '../actions/UserActions'
class UserContainer extends React.Component {
render() {
const { user, handleLogin } = this.props
return (

)
}
}
const mapStateToProps = store => {
return {
user: store.user,
}
}
const mapDispatchToProps = dispatch => {
return {
handleLogin: () => dispatch(handleLogin()),
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(UserContainer)

Теперь внимание: в компоненте App есть два "независимых компонента". Сейчас при
изменении данных в редьюсере для Page - User перерисовываться не будет. App тоже,
само собой. App у нас вообще не будет перерисовываться более при таком раскладе.
Снова покликаем по кнопкам (console.log в



остался):

93

Оптимизация перерисовок

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

console.log

) сколько раз у вас что рендерится, и нет ли чего лишнего.

Давайте заодно здесь быстренько исправим отрисовку кнопок в



src/components/Page.js

94

Оптимизация перерисовок

...
export class Page extends React.Component {
onBtnClick = e => {
...
}
renderButtons = () => {
const years = [2018, 2017, 2016, 2015, 2014]
return years.map((item, index) => { // [1]
return (

{item}

)
})
}
renderTemplate = () => {
...
}
render() {
const { year, photos } = this.props
return (

{this.renderButtons()}

{year} год [{photos.length}]

{this.renderTemplate()}

)
}
}
...

(Добавьте по вкусу щепотку

margin

для

.btn

)

[1] Использовать в данной ситуации index для key плохо?. В данном случае - не плохо.
Напоминаю, что индекс в качестве ключа плохо использовать, когда у вас элементы
меняются местами. Справедливости ради, здесь в качестве индекса можно было бы
использовать "год", так как главное в индексе - это уникальность.

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

95

Оптимизация перерисовок

96

Доработки

Доработки
Доработки кладутся в master ветку. К некоторым будут комментарии.
Чтобы легко разобраться в коде, который изменился - открывайте PR #X ссылки, где в
каждом pull-request'e видно список измененных файлов (таб - files).

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

загрузка фото после успешной авторизации (PR #3)
Сделано достаточно просто с помощью callback-функции.

97

Доработки

модальное окно с большим фото в нем (PR #5)
Добавлен пакет react-modal, немного стилей и переделана логика отображения фото.
Появился компонент




, который содержит все фото (компонент

) + модальное окно (одно!). Из



адрес для большого изображения в компонент



прелоадера использован трюк с подгрузкой изображения в

по клику передается url, в котором для
img.onload

.

Больше подробностей на вебинаре (время старта ХХ) [1]
[1] будет добавлена ссылка, когда выложу запись на YT-канал

Redux-saga версия
Версия с сагой расположена в отдельной ветке.
Есть парочка

TODO:

в коде, можете присылать PR.

Продолжение следует...

98

Что дальше?

В качестве заключения
Я надеюсь вы выполняли код по ходу книги? Если нет, то настало время взять и
сделать финальный пример лично.
Прикладываю план по закреплению знаний и прокачке:
0) Ознакомиться с React-router'ом по официальной документации [EN].
0a) По роутингу есть статья на сайте (текст и видео)
1) Тестовое задание #1
2) Тестовое задание #2
Если у вас заблокирован VK, то задания можно найти в github-репозитории. Можете
подписаться на репозиторий, чтобы не пропустить следующие.
На уже прошедшие тестовые задания, можно получить code-review платно, стоимость
15$, присылайте запрос на почту: maxpfrontend@gmail.com с темой "Code review
тестовое задание X", где X - номер.
3) Тестирование логики (reducers и actions), включая мок асинхронного запроса.
4) Тестирование компонентов Jest + enzyme
5) Добавьте тесты для второго тестового задания, прокачайте его на свое усмторение

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

Если вам понравилось, вы можете поддержать проект или оставить отзыв.

Полезные ссылки
Мои уроки/вебинары/соц.сети:

99

Что дальше?

Полноценный учебник "Основы React"
Расписание стримов и вебинаров (на сайте есть текстовые версии вебинаров)
Youtube канал c записями вебинаров и стримов
Группа vkontakte
Канал в telegram
Twitter
Facebook

100

Спасибо

Спасибо
При создании данного учебника и в процессе его жизни, мне помогали:
Артем Бочков;

101