table of contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5...
TRANSCRIPT
1.1
1.2
1.2.1
1.2.2
1.2.3
1.2.4
1.2.5
1.2.6
1.3
1.3.1
1.3.2
1.3.3
1.3.4
1.3.5
1.3.6
1.3.7
1.3.8
1.3.9
1.3.10
1.3.11
1.3.12
1.3.13
1.4
TableofContentsВступление
Подготовка
Созданиеpackage.json
Первыешаги
ES2015,ReactHMR
ESLint
Установказависимостейнаавтомате
Reactdevtools
Создание
ОсновыRedux(теория)
Точкавхода
НастройкаStore
СозданиеReducer
Присоединениеданных(connect)
Комбинированиередьюсеров
Контейнерыикомпоненты
Созданиеactions
Константы
Наводимпорядок
Middleware(усилители).Логгер
Асинхронныеactions
ВзаимодействуемсVK
Заключение
2
ReactRedux[RUtutorial]Всемдоброговременисуток.Вданномучебномкурсевынайдете2раздела:
1. Подготовка2. Теорияreduxисозданиевеб-приложенияпошагам
Есливаминтересно,именновыполнениеасинхронныхдействийиработасVKAPI,выможетепрочитатьсоответствующиеглавы.
Курспредополагает,чточитательужезнакомсReact.js.Есливынезнакомы,рекомендуюдляначалаознакомитьсяскурсомReact.jsдляначинающих.
РезультатомизучениядолжностатьумениеучитателяразрабатыватьприложенияспомощьюRedux,опираясьстрогонаоднонаправленныйпотокданныхидругиеправиладаннойтехнологии.Курсохватываетвсетиповыевопросы(какпоказатьpreloader,какотобразитьошибкуврезультатеасинхронногозапросаит.д.),нонезатрагиваетвопросысерьезнойоптимизациисборкиwebpack'аинаписаниятестов.
Консультациииплатныеуслуги
Общениепоскайпу-1900руб/час,минимум-30минут.
Созданиесервисовсиспользованиемreact/redux,поискпроблемспроизводительностью,помощьвсобеседованииразработчиков-ценапозапросу.
Пишитена[email protected]стемой"КонсультацияReact/Redux"
Полезныессылки
React.js(EN)-офф.сайт,содержитпримерыдляизучения
React.jsдляначинающих(RU)-курснарусскомдлятех,ктохочетпостичьазыreact.js
Redux(EN)-офф.сайт,которыйполностьюохватываетвопросизученияreduxисодержитотличныепримеры.
Redux(RU)-переводофф.документациивкоторомпереведенопрактическивсе.
Webpackscreencast(RU)-скринкастотИльиКанторапоwebpack
Вступление
3
Twitterаккаунтсоздателяredux,иегобесплатныйвидеокурс(EN)
МойTwitter-можетезадаватьвопросы.
Вступление
4
ПодготовкаДаннаяглаваявляетсяобучающейдлялюдей,которыеневкурсе,илихотятосвежитьипополнитьсвоюбазузнаний,последующимпунктам:
созданиесписказависимостейпроектанастройкаWebpack
созданиеdev-сервера(Express.js)ES2015/ES7(Babel6)Babel6+ReactReacthotreloadESlint
Результатомподготовки,будетследующийкод.
Есливампонятенкодданногораздела,предлагаюсразупереходитькглаве"Создание".
Длявсехостальных,япредлагаюзанесколькопростыхшаговнастроитьудобноерабочееокружение.
Подготовка
5
Созданиеpackage.jsonВдиректорииспроектомнеобходимвыполнить:
npminit
иответитьнавопросы.
NPMсоздаcтдлянашегоприложенияфайл-описание.Онпригодитсянамдляуказаниязависимостейипрочихвкусностей.
Первымделоммыдобавимвpackage.jsonкомандудлястартасервера(которыйсоздадимнаследующемшаге).Дляэтогонеобходимодобавить:
"scripts":{
"start":"nodeserver.js"
}
Итоговыйфайлвыглядитпримернотак:
package.json
{
"name":"redux-ru-tutorial",
"version":"1.0.0",
"description":"ReduxRUtutorial",
"main":"index.js",
"scripts":{
"start":"nodeserver.js"
},
"author":"MaximPatsianskiy",
"license":"MIT"
}
Теперьеслинаписатьnpmstartбудетвыполненакомандаnodeserver.js,котораянампонадобитсяпозжедлязапускасервера.
Также,померероставашегоприложения,выбудетеустанавливатьновыепакетысфлагом--saveили--save-dev.Такимобразом,когдавы,либокто-тодругойбудетразворачиватьвашпроект,достаточнобудетнаписатьnpminstall,чтобыустановитьвсезависимости.
Созданиеpackage.json
6
Созданиеpackage.json
7
ПервыешагиДлясборкинашегокодабудемиспользоватьWebpack.
npmiwebpackwebpack-dev-middlewarewebpack-hot-middleware--save-dev
Флаг--save-dev-добавитпакетwebpack(ипарочкувспомогательных)всписокзависимостейнашегопроекта.
Теперьнеобходимосоздатьконфигурационныйфайл
webpack.config.js
varpath=require('path')
varwebpack=require('webpack')
module.exports={
devtool:'cheap-module-eval-source-map',
entry:[
'webpack-hot-middleware/client',
'./src/index'
],
output:{
path:path.join(__dirname,'dist'),
filename:'bundle.js',
publicPath:'/static/'
},
plugins:[
newwebpack.optimize.OccurenceOrderPlugin(),
newwebpack.HotModuleReplacementPlugin(),
newwebpack.NoErrorsPlugin()
]
}
Дажедлятех,ктонезнакомсwebpack'ом,этотконфигпокажетсявполнепонятным.Вentry-указываетсяоткудаwebpack'уначинатьсборку,авoutput-кудасгенерировать.Вdevtoolуказываем,чтонамнуженsource-mapдляотладкикодаспаройнастроек(cheap,module,eval).
Интереспытливогочитателяможетвызватьстрока'webpack-hot-middleware/client'(NPM),котораянужнанамдляподдержкиhot-reload,вместесоднимизплагинов-webpack.HotModuleReplacementPlugin
Первыешаги
8
ServerНампонадобитсясервердляразработки,дляэтогомыможемиспользоватьwebpack-dev-server,либобыстроразвернемсвой.Вданномруководстверассматриваетсявторойвариант.
Дляначалаустановимexpress
npmiexpress--save-dev
исоздадимнашweb-сервернаегооснове.
server.js
varwebpack=require('webpack')
varwebpackDevMiddleware=require('webpack-dev-middleware')
varwebpackHotMiddleware=require('webpack-hot-middleware')
varconfig=require('./webpack.config')
varapp=new(require('express'))()
varport=3000
varcompiler=webpack(config)
app.use(webpackDevMiddleware(compiler,{noInfo:true,publicPath:config.output.publi
cPath}))
app.use(webpackHotMiddleware(compiler))
app.get("/",function(req,res){
res.sendFile(__dirname+'/index.html')
})
app.listen(port,function(error){
if(error){
console.error(error)
}else{
console.info("==>Listeningonport%s.Openuphttp://localhost:%s/inyourbro
wser.",port,port)
}
})
Обратитевнимание,настроку
app.use(webpackHotMiddleware(compiler))
Первыешаги
9
Наэтомшагедобавляетсянемногомагиикнашемусерверу,аименно:сервертеперьпринимаетуведомления,когдаглавныйjsскриптсобранивызываетобновлениямодулейнашегоприложения,востальномcервервсегонавсегоотдаетнамindex.html,вкотороммыподключаемфайл,сгенерированныйwebpack'ом.
index.html
<!DOCTYPEhtml>
<html>
<head>
<title>Redux[RU]Tutorial</title>
</head>
<body>
<divid="root">
</div>
<scriptsrc="/static/bundle.js"></script>
</body>
</html>
Добавимточкувходадляwebpack'а.Мыуказалиеевнашемwebpack.config.jsвнастройкеentry-index.js
src/index.js
document.getElementById('root').innerHTML='Привет,яготов.'
Проверим?
npmstart
http://localhost:3000/
Отлично,кое-чтоужезавелось.Ноеслисейчасобновитькодвфайлеindex.jsстраницавбраузереостанетсяпрежней.Хотяприэтомвконсолимыувидим,чтоwebpackчто-топересобрал.
HotReloadДеловтом,что"модуль"index.jsнеумеетсообщатьwebpack'укакимобразомонхотелбыобновиться.Сейчас,дляпростотыпримера,достаточнодобавитьстрокуmodule.hot.accept(),котораясообщаетwebpack'уследующуюинформацию:"Я(index.js)умеюhot-reloadсам,дляэтогопростовозьмииобновименявсгенерированномфайле(/static/bundle.js)."
Первыешаги
10
src/index.js
document.getElementById('root').innerHTML='Привет,яготов!'
module.hot.accept()
Перезапуститесервер,обновитестраницубраузера.
Атеперьпоменяйтетекст,вindex.js-онтакжеобновитсянаэкранебраузера.Браузернеперезагрузитстраницу,каквслучаесlive-reload,асразуотобразиттольконужныйкусочек.Этогораздоудобнее!
Конечно,постоянноуказыватьmodule.hot.accept()неудобно,инеразумно.Следующийшагпоможетнамизбавитьсяот"ручной"настройкиhot-reloadдляreact.jsкода.
Первыешаги
11
ES2015,ReactHMRДляиспользованиявозможностейES6(2015)иES7будемиспользоватьbabel.Свыходом6йверсии,онсталоченьмодульным,поэтомунепугайтесьбольшомуколичествузависимостей.
Babel6Всеначинаетсяс
npminstallbabel-corebabel-loader--save-dev
Далеенужнопоставитьпресеты(предустановки),которыенамнужны.
#ДляподдержкиES6/ES2015
npminstallbabel-preset-es2015--save-dev
#ДляподдержкиJSX
npminstallbabel-preset-react--save-dev
#ДляподдержкиES7
npminstallbabel-preset-stage-0--save-dev
Намоднозначнонуженполифил,чтобывсефичиработаливбраузере
npminstallbabel-polyfill--save
Инемногоулучшимвремясборки,добавивследующиепакеты
npminstallbabel-runtime--save
npminstallbabel-plugin-transform-runtime--save-dev
*Длянаписанияэтогонебольшоготекстапроbabel6,яиспользовалстатьюUsingES6andES7intheBrowser,withBabel6andWebpack*
Вданныймомент,унасдостаточно"пакетов",чтобыписать"современный"кодииспользоватьJSX.Давайтевэтомубедимся.
Во-первых,подправимконфигдляwebpack'а:
ES2015,ReactHMR
12
webpack.config.js
varpath=require('path')
varwebpack=require('webpack')
module.exports={
devtool:'cheap-module-eval-source-map',
entry:[
'webpack-hot-middleware/client',
'babel-polyfill',
'./src/index'
],
output:{
path:path.join(__dirname,'dist'),
filename:'bundle.js',
publicPath:'/static/'
},
plugins:[
newwebpack.optimize.OccurenceOrderPlugin(),
newwebpack.HotModuleReplacementPlugin(),
newwebpack.NoErrorsPlugin()
],
module:{//Обновлено
loaders:[//добавилиbabel-loader
{
loaders:['babel-loader'],
include:[
path.resolve(__dirname,"src"),
],
test:/\.js$/,
plugins:['transform-runtime'],
}
]
}
}
Добавиласьзаписьвсекцииloaders.Теперьвсеjsфайлывsrcдиректориибудутобрабатыватьсяbabel-loader'ом,которомумывсвоюочередьтожедолжныуказатьнастройки.Дляэтого,нужносоздатьфайл.babelrcсоследующимсодержимым:
{
"presets":["es2015","stage-0","react"]//поддержкаES2015,ES7иJSX
}
Есливызнакомысgulp,томожнопровестинекуюаналогию,междуплагинамиgulpилоадерами(loaders)webpack'a.Еслимыхотимделатькакие-топреобразованияскодомвнутрифайла,будьтоcss,jsиликартинки-мыиспользуемсоответсвующий
ES2015,ReactHMR
13
loader.Причемсоздаватьдополнительныефайлынастроек,каквслучаесbabel,обычноненужно.
Ок,создадимReactкомпонент,незабывприэтомскачатьнужныепакеты:
npmireactreact-dom--save
src/index.js
import'babel-polyfill'
importReactfrom'react'
import{render}from'react-dom'
importAppfrom'./containers/App'
render(
<App/>,
document.getElementById('root')
)
src/containers/App.js
importReact,{Component,PropTypes}from'react'
exportdefaultclassAppextendsComponent{
render(){
return<div>ПриветизApp</div>
}
}
Перезапускаемсборку(npmstart).
ВеськоднатекущиймоментвыложенвспециальнуюветкунаGithub.Можетесверится,есличто-тонеработает.
React+HotReloadВозможно,вамвстретитсяаббревиатураHMR(hotmodulereplacement),чтовпринципеболееправильноотражаетсуть,поэтомуподhot-reloadяподразумеваюименноHMR;)
Каквыпомнитеизпрошлойглавы-мыдобавилиmodule.hot.accept(),длятого,чтобыwebpackобновлялкодизфайлаindex.jsвсборкебезперезагрузкистраницывбраузере.Еслисейчаспопробоватьизменитьчто-товApp.js-товрезультатеничего
ES2015,ReactHMR
14
неслучится,ровнопотемжепричинам,чтоивпредыдушемслучае.Чтож,этопоправимоиблагодарядобрымлюдям,намненужносамимвписыватьacceptфункцию.Итак,встречайте(иустанавливайте):
npminstallreact-hot-loader--save-dev
Достаточнодобавитьещеодинloaderвконфигимыполучимhot-reloadдляReactкомпонентов.
varpath=require('path')
varwebpack=require('webpack')
module.exports={
devtool:'cheap-module-eval-source-map',
entry:[
'webpack-hot-middleware/client',
'babel-polyfill',
'./src/index'
],
output:{
path:path.join(__dirname,'dist'),
filename:'bundle.js',
publicPath:'/static/'
},
plugins:[
newwebpack.optimize.OccurenceOrderPlugin(),
newwebpack.HotModuleReplacementPlugin(),
newwebpack.NoErrorsPlugin()
],
module:{
loaders:[
{
loaders:['react-hot','babel-loader'],//добавилиloader'react-hot'
include:[
path.resolve(__dirname,"src"),
],
test:/\.js$/,
plugins:['transform-runtime'],
}
]
}
}
Перезапускаемсборкуипроверяем.ТеперьHMRработаетидляReactкомпонентов,еслиженет-сверьтесьсисходнымкодомданногораздела.
ES2015,ReactHMR
15
Длятого,чтобыначатьписатькодredux-приложения,яосновательнорекомендуюнастроитьESLint,чтобыбыстрорешатьсинтаксическиеошибкииповыситьпроизводительность.Этиммызаймемсянаследующемшаге.
P.S.ВофициальномрепозиторииReact-hot-reloader'aговоритсяотом,чтоготовитсяквыходуReactTransform,которыйстанетлогическимпродолжениемтекущихрешений.(31.01.2015)
P.P.S.Какговоритсоздательбиблиотекиreact-hot-reload,намбольшененужноиспользоватьwebpack.NoErrorsPlugin,которыйранеевыполнялследующее:есливсборкебылиошибки,оннеобновлялфайлсборки.Поэтомупростоудалитесоответствующуюстрокуизсекцииpluginsвнутриwebpack.config.js
ES2015,ReactHMR
16
ESlintЕсливынезнакомыс"линтерами",товы,наверняка,знакомыстипичнымпоискомошибкивстилеmyVariableisundefinedиподобными.
Настроивлинтер,высможетевидетьвконсолимногополезнойинфорамции:отзабытойточки-с-запятой(кстати,неактуальнодляES2015),доуведомленийонеиспользуемыхпеременных.Оченьудобнодлярефакторингакода.
СовременныйESlintпошелещедальше.Сдобавленимсобственныхправил,выможетеподдерживатьединыйстильпрограммированиявнутрикомпании!
Но,довольнотеории.
Поставимнужныепакеты:
npmibabel-eslinteslinteslint-plugin-react--save-dev
Теперь,хотяяиговорил,чтофайлы.xxxrcобычноненужны,дляESlintвсеженужносделатьтакой.Внеммыопишемправиладлясинтаксическойпроверки(lint)кода.
.eslintrc
ESLint
17
{
"extends":"eslint:recommended",
"parser":"babel-eslint",
"env":{
"browser":true,
"node":true
},
"plugins":[
"react"
],
"rules":{
"no-debugger":0,
"no-console":0,
"new-cap":0,
"strict":0,
"no-underscore-dangle":0,
"no-use-before-define":0,
"eol-last":0,
"quotes":[2,"single"],
"jsx-quotes":[1,"prefer-single"],
"react/jsx-no-undef":1,
"react/jsx-uses-react":1,
"react/jsx-uses-vars":1
}
}
Самоеинтересное,конечноже,секцияrules,где:
0-правиловыключено1-правиловыдастпредупреждение2-правиловыдастошибку
Некоторыеправилапринимаютмассиваргументов,напримерquotes.Внашемслучае,именноэтоправиломожнопрочитатьтак:Показывайошибку,есливстретишьдвойнуюкавычку.
Списоквсехправилeslint-plugin-react.
ЧтобыESlintработалвавтоматическомрежиме,мыбудемвсетакжеиспользоватьwebpack.
Нарядуссекциейloaders,вwebpackестьсекция...preloaders(да-да,postloadersтожеесть).Ядумаюизназваниясекцийужевсепонятно:кодобрабатывается"до"и"после"loaders.ДляESlintнамподходитpreloaders.
Итак,поставимнужныйлоадер:
npmieslint-loader--save-dev
ESLint
18
Поправимконфиг:
webpack.config.js
...
module:{
preLoaders:[//добавилиESlintвpreloaders
{
test:/\.js$/,
loaders:['eslint'],
include:[
path.resolve(__dirname,"src"),
],
}
],
loaders:[//всеостальноеосталосьнетронутым
{
loaders:['react-hot','babel-loader'],
include:[
path.resolve(__dirname,"src"),
],
test:/\.js$/,
plugins:['transform-runtime'],
}
]
}
...
Здесьивбудущем,ябудуиспользовать...-еслидаюфрагмент(ы)файла,аневеськодцеликом.ВеськодразделавсегдаестьнаGithub,ссылкауказанавконцестатьи.
Теперьдостаточноперезапуститьсборку.Должнополучитьсяследующее:
Линтерпоказыватенам,чтовфайлеsrc/containers/App.jsестьнеиспользуемаяпеременнаяPropTypes,хотяонаопределена.Этодействительнотак,поэтомудавайтепоправимкод:
ESLint
19
src/Containers/App.js
importReact,{Component}from'react'
exportdefaultclassAppextendsComponent{
render(){
return<div>ПриветизApp</div>
}
}
Сохранивфайл,мыувидимвконсолиследующее:
Великолепно!Ошибокнет.Навсякийслучайдобавлю:сборкуwebpackненужнобылоперезапускать.Обычно,сборкунужноперезапускатьлишьпослеизмененийвwebpack.config.jsВостальныхслучаях,таккакунаснастроен"режимнаблюдения"-webpackсамперезапуститсяисгенерируетновыйфайлсборки.
Итого:наданныймоментмыможемписатьES2015/ES7код,использоватьJSXинепереживатьзаглупыеошибки,асвоевременноправитьих,используяподсказкиESlint.Webpackавтоматическипересобираетнашфайлсборки(/static/bundle.js),приэтоммыиспользуемвсюмощьHotModuleReplacement,иеслиизменимчто-либовjsкодеreact-компонентов-измененияприлетятсразужевбраузербезперезагрузкистраницы.Поздравляю,мыготовыскомфортомнаписатьReduxприложение.
Исходныйкодданногораздела
Длянаписанияэтогораздела,яиспользовалследующиематериалы:
LintinginWebpack(ENG,текст)
ESLint
20
УстановказависимостейнаавтоматеВнутриэтогоразделаяставилвсезависимостиспомощьюnpminstall<имя_пакета>,ноестьболееудобныйспособ-использоватьплагиндляwebpack-npm-install-webpack-plugin
npminstallnpm-install-webpack-plugin--save-dev
Плагинбудетанализироватьнашифайлынапредметзависимостейиустанавливатьновыепакеты,еслиобнаружитсянеизвестнаязависимость.Главное,незабывайтеудалятьлишниепакеты,еслибудетеэкспериментироватьипробоватьразные.
.npmrc
save=true
save-exact=true
webpack.config.js
...
//добавьтеновуюзависимостьвначалеконфига
varNpmInstallPlugin=require('npm-install-webpack-plugin');
...
//добавьтеплагинвсекциюплагинов
plugins:[
newwebpack.optimize.OccurenceOrderPlugin(),
newwebpack.HotModuleReplacementPlugin(),
newNpmInstallPlugin()//<--
],
...
Далеевруководстве,явсеравнобудуписатьnpminstall,таккакэтовизуальнодаетхорошеепредставлениеотом,какиезависимостинамнужны.Есливыпоставилиинастроилиnpm-install-webpack-plugin,томожетенебеспокоитсяипропускатьэтистроки.
Установказависимостейнаавтомате
21
ReactdevtoolsЕслиуваснеустановленодополнениедляконсолихрома(ровнокакисамGoogleChrome),рекомендуювамэтосделать,таккакиногдамыбудемобращатьсякэтиминструментам.
Reactdevtoolsвмагазинерасширений.
Есливыиспользуетевозможностьконсолихрома-$0,товероятновампонравитсяивозможность$r.
Какработает$r?(нижеgif-анимация)
Длятех,кточитаетвформатекниги:необходимокликнутьнакомпонентвконсолихрома,навкладкеReact,далеепереместитьсянавкладкуConsoleинаписать$r.Втакомслучае,вконсоликомпонентбудетпредставленвсвоейнативнойjs-реализации.
Reactdevtools
22
СозданиеЯпредлагаюпошагамсоздатьодностраничноеприложение,сминимумомфункций,котороепослелогинаиподтвержденияправдоступакфото,будетвыдаватьтопваших"залайканных"фотовпорядкеубывания.Схематично,приложениеможнопредставитьследующимобразом:
Преждечемописыватьструктуру,давайтевобщихчертахвзглянемнаRedux.
Redux-приложениеэто:
состояние(state)приложенияводномместеоднонаправленныйпотокданных
ReduxвдохновленFluxметодологиейиязыкомпрограммированияElm
Создание
23
Подкапотом,Reduxиспользуетслабодокументированнуюфичуреакта-context,которая,кслову,досихпорявляетсяunstable,иможетбытьизменена/удалена.Ксчастью,этогонепроисходитиврядлипроизойдет.
Файлыипапки:Изначальнонашеприложениевфайловомменеджередолжновыглядетьтак:
+--src
|+--actions
|+--components
|+--constants
|+--containers
|+--reducers
|+--index.js
+--index.html
+--package.json
+--server.js
+--webpack.config.js
Создание
24
ОсновыRedux(теория)Курсрассчитаннасозданиеприложенияпошагам,аэтозначитмаксимумпрактикииминимумтеории.Тотсамыйминимум,передвами.
Давайтеещеразвзглянемнасхемунашегоприложения:
Вшапкеслевазаголовокитрикнопкивыборагода.Ниже-фотосоответствующегогода,отсортированноепоколичествулайков.
Вшапкесправа-ссылкавойти/выйти.
Представим,какдолжнывыглядетьданныедлятакойстраницы:
ОсновыRedux(теория)
25
app:{
page:{
year:2016,
photos:[photo,photo,photo...]
},
user:{
name:'Имя',
...
}
}
Поздравляювас,мытолькочтоописаликакдолжновыглядетьсостояние(state)нашегоприложения.
Засодержаниевсегосостояниянашегоприложения,отвечаетобъектStore.Какуженеразупоминалось-этообычныйобъект.Важно,чтовотличииотFlux,вReduxтолькоодинобъектStore.
Нехочетсяоставлятьваснадолгобезпрактики,поэтомупроцесссозданияstoreинемногоподробностейпронегояаккуратновплетувследующиеглавы,апокадостаточнотого,что:store,"объединяет"редьюсер(reducer)идействия(actions),атакжеимеетнесколькочрезвычайнополезныхметодов,например:
getState()-позволяетполучитьсостояниеприложения;dispatch(actions)-позволяетобновлятьсостояния,путемвызовадействия;subcribe(listener)-регистрируетслушателей
Actions
Actionsописываютдействия.
Actions-этопростойобъект.Обязательноеполе-type.Также,еслиследоватьсоглашению,вседанные,которыепередаютсявместесдействием,кладутвнутрьсвойстваpayload.Такимобразом,длянашегоприложения,мыможемсоставить,напримертакуюпаруactions:
{
type:'ЗАГРУЗИ_ФОТО',
payload:2016//год
}
ОсновыRedux(теория)
26
{
type:'ФОТО_ЗАГРУЖЕНЫ_УСПЕШНО',
payload:[массивфото]
}
Чтобывызватьactions,мыдолжнынаписатьфункцию,котораяврамкахFlux/Reduxназывается-ActionsCreator(создательдействия),нопередэтимстоитпринятьвовнимание,чтообычнотипдействия,описываюткакконстанту.Например,константывашегопроекта:
constGET_PHOTO_REQUEST='GET_PHOTO_REQUEST'
constGET_PHOTO_SUCCESS='GET_PHOTO_SUCCESS'
Возникаетвопрос,зачем?Вмаленькихпроектах-незачем.Вбольших-этоудобно.Пока,простозапомните.
Вернемся,кActionsCreator,одинизнаших"создателейдействий",выгляделбытак:
functiongetPhotos(year){
return{
type:GET_PHOTOS,
payload:year
}
}
Итого:actionsсообщаетнашемуприложению-"Эй,что-топроизошло!Иязнаю,чтоименно!"
Reducer
"Actionsописываетфакт,чточто-топроизошло,нонеуказывает,каксостояниеприложениядолжноизменитьсявответ,этоработадляReducer'а"-(офф.документация)
Нашеприложениененуждаетсявнесколькихредьюсерах,нокрайненеобходимопознакомитьчитателясreducercomposition,таккакэтофундаментальныйшаблонпостроенияreduxприложений:мыразбиваемнашеглобальноесостояниенакусочки,закаждыйкусочекотвечаетсвойreducer.КусочкиобъединяютсявКорневомРедьюсере(rootReducer).
ОсновыRedux(теория)
27
Схематично,нашеприложениеможнопредставитьтак:
Таккакунасестьreducer'ыpageиuser,можнопредставитьследующийдиалог:
pageActions:Пришло123фото
Reducer(page):Ок,нужноположитьэти123фотовpage.photos
Анаjsвыгляделобытак:
functionpage(state=initialState,action){
switch(action.type){
caseGET_PHOTO_SUCCESS:
returnObject.assign({},state,{
photos:action.payload
})
default:
returnstate
}
}
Обратитевнимание,мынемутировалинашstate,мысоздалиновыйstate.Этоважно.Крайневажно.Вредьюсере,мывсегдадолжнывозвращатьновыйобъект,анеизмененныйпредыдущий.
ОсновыRedux(теория)
28
Напрактике,ябудуиспользоватьobjectspreadsyntax,поэтомупредыдущуюфункциюсObject.assignможнопереписатьследующимобразом:
functionpage(state=initialState,action){
switch(action.type){
caseGET_PHOTO_SUCCESS:
return{...state,photos:action.payload}//Objectspreadsyntax
default:
returnstate
}
}
Объект,которыймывозвращаемвредьюсере,далееспомощьюфункцииconnect,превратитсявсвойствадлякомпонентов.Такимобразом,еслипродолжитьпримерсфото,томожнонаписатьтакойпсевдо-код:
<Pagephotos={reducerPage.photos}/>
Благодаряэтому,внутрикомпонента<Page/>,мысможемполучитьфото,какthis.props.photo
Япостаралсяоченькраткодатьсамуюважнуютеорию.
Есличто-тоосталосьнепонятным,непереживайте,напрактикемывсезакрепимитогдавсевстанетнасвоиместа.
ОсновыRedux(теория)
29
ТочкавходаПодтянемReduxиreact-reduxвнашпроект:
npmireduxreact-redux--save
Какужебылоописановразделе"подготовка",точкавходавнашеприложение-src/index.js
Обновимегосодержание:
src/index.js
importReactfrom'react'
import{render}from'react-dom'
import{createStore}from'redux'
import{Provider}from'react-redux'
importAppfrom'./containers/App'
conststore=createStore(()=>{},{})//WAT;)
render(
<Providerstore={store}>
<App/>
</Provider>,
document.getElementById('root')
)
Итак,первыйкомпонентизмираRedux-<Provider/>([EN]документация).
Благодаряэтомукомпоненту,мысможемполучатьнеобходимыеданныеизstoreнашегоприложения,есливоспользуемсявспомогательнойфункциейconnect,речьокоторойпойдетдалее.Сейчаснамиполучатьнечего,таккакstoreунас-пустойобъект.
Давайтеподробнеепосмотримнастроку:
conststore=createStore(()=>{},{})
Во-первых,есливамтрудночитатьES2015код,топереводитееговпривычныйES5,спомощьюbabel-playground.
Точкавхода
30
Во-вторых,давайтевзглянемнадокументациюметодаcreateStore:принимаетодинобязательныйаргумент(функциюreducer)ипарочкунеобязательных(начальноесостояниеи"усилители").
Теперьпереведемто,чтомынаписали,когдаприсваивалиstore:Возьмипустуюанонимнуюфункциювкачествередьюсераипустойобъектвкачественачальногосостояния.Есликоротко:возьминичегои"ничего"неделай.
Предлагаювынестисозданиеstoreвотдельныйфайл,длятого,чтобыдобавитьвозможностьHMRидляболееудобнойработысreducer'омиусилителями(enhancers).
src/index.js
importReactfrom'react'
import{render}from'react-dom'
import{Provider}from'react-redux'
importAppfrom'./containers/App'
importconfigureStorefrom'./store/configureStore'
conststore=configureStore()
render(
<Providerstore={store}>
<App/>
</Provider>,
document.getElementById('root')
)
Усилители-этоmiddlewareфункции.Есличитательзнакомсexpress.js,тоонзнакомсусилителямивredux.Дляостальных:типичныйусилитель-логгер(logger),которыйпростопишетвконсольвсечтопроисходитснаблюдаемымобъектом.
Еслипосмотретьвконсоль,webpackвсетакжеусердноработаетисообщаетобошибке:нетфайлаconfigureStore...Порасоздать,атекущийкодможновзятьздесь.
Точкавхода
31
НастройкаStoreДляначала,повторимто,чтонамужеизвестнопроstore,ибытьможетдобавимчуть-чутьнового.Итак:
Storeхранитсостояниеприложения.Единственныйпутьизменитьstore-этоотправитьдействие(dispatchaction).
Store-этонекласс.Этообычныйобъектснесколькимиметодами,аименно:
getState()
dispatch(action)
subscribe(listener)
replaceReducer(nextReducer)
Создадимфункциюконфигурацииstore
store/configureStore.js
import{createStore,applyMiddleware}from'redux'
importrootReducerfrom'../reducers'
exportdefaultfunctionconfigureStore(initialState){
conststore=createStore(rootReducer,initialState)
returnstore
}
Ничегонеобычного,строгоподокументации:передаемrootReducerвтолькочтоимпортированнуюфункциюcreateStore.
ВReduxверсии2.x.xмыдолжныявноуказать,чтоreducersподдерживаютhotreload.Сделатьэтодостаточнопросто.Есливзглянутьвначалокода,видно,чтомыимпортируемтакназываемыйкорневойредьюсер(rootReducer),которыйпосутииотражаетвсесостояниенашегоприложения.Теперьпосмотримещевышепотуториалу-ага,уstoreестьподходящаяфункция-replaceReducer
ТеперьвзявзаосновуотличныйвидеоскринкастпроWebpack,мызнаем,чтоhotreloadожидаетотнасфункцииaccept.Вуаля,поравноситьправки.
store/configureStore.js
НастройкаStore
32
import{createStore}from'redux'
importrootReducerfrom'../reducers'
exportdefaultfunctionconfigureStore(initialState){
conststore=createStore(rootReducer,initialState)
if(module.hot){
module.hot.accept('../reducers',()=>{
constnextRootReducer=require('../reducers')
store.replaceReducer(nextRootReducer)
})
}
returnstore
}
Ксожалению,нашкоддосихпорнеработает.Webpackругаетсянаотсутствующийreducer.Давайтеисправимэто,ияобещаю,наконец-томожнобудетпосмотретьнарезультатвбраузере.
НастройкаStore
33
СозданиеReducerПо-моему,вэтомкоденечегодажекомментировать!
src/reducers/index.js
constinitialState={
user:'UnknownUser'
};
exportdefaultfunctionuserstate(state=initialState){
returnstate;
}
Пораоткрытьбраузерипосмотретьвконсольразработчика.Еслитаместьпредупреждения/ошибки-значит,где-тоHMRнесработал.Достаточнообновитьбраузер.Все-такиспоследнегоразамысоздалидостаточнофайлов,чтобыреакт/вебпакрастерялдолюсвоеймагииипопросилбынасобновитьстраницу.
Всеок,ноничегоинтересного.Вследующейглаведобавимвыводимениюзера.
СозданиеReducer
34
Связываниеданныхизstoreскомпонентамиприложения
Вразделе"Точкавхода"шларечьонекойфункцииconnect,котораяпоможетнамполучитьвкачествеpropsдлякомпонента<App/>данныеизstore.Добавимее:
src/containers/App.js
importReact,{Component}from'react'
import{connect}from'react-redux'
classAppextendsComponent{
render(){
return<div>ПриветизApp,{this.props.user}!</div>
}
}
functionmapStateToProps(state){
return{
user:state.user
}
}
exportdefaultconnect(mapStateToProps)(App)
Назначениефункцииconnectвытекаетизназвания:подключиReactкомпоненткReduxstore.
Результатработыфункцииconnect-новыйприсоединенныйкомпонент,которыйоборачиваетпереданныйкомпонент.
Унасбылкомпонент<App/>,анавыходеполучился<Connected(App)>.Вэтомнетрудноубедиться,есливзглянутьвreactdevtools.
Присоединениеданных(connect)
35
Взглянитенаправуючастьскриншота,ивыувидите,чтовсвойствах(props)нашегокомпонента<App/>теперьестьметодreduxstore-dispatch,иобъект(внашемслучае,покачтострока)user.Этотакжерезультатработыфункцииconnect.
Давайтеещепоиграемсяспростымпримером.Дляначалаизменимнаборданных:
src/reducers/index.js
constinitialState={
name:'Василий',
surname:'Реактов',
age:27
}
exportdefaultfunctionuserstate(state=initialState){
returnstate
}
затемподкрутимкомпонент:
src/containers/App.js
Присоединениеданных(connect)
36
importReact,{Component}from'react'
import{connect}from'react-redux'
classAppextendsComponent{
render(){
const{name,surname,age}=this.props.user
return<div>
<p>ПриветизApp,{name}{surname}!</p>
<p>Тебеуже{age}?</p>
</div>
}
}
functionmapStateToProps(state){
return{
user:state
}
}
exportdefaultconnect(mapStateToProps)(App)
Всеработаетровнотак,какмыуказали:вобъектuser"подключилось"всесостояниенашегоприложенияstate,котороесейчасоченьпростоеиописановsrc/reducer/index.js.
Преждечеммыперейдемксозданиюactionsивзаимодействиюпользователясостраницей,давайтепоговоримокомбинированииредьюсеров(combineReducers)исоздадимреальнуюструктурунашегобудущегоприложения.
Исходныйкоднатекущиймомент.
P.S.есливыпереживаете,чтоHMRвданныймоментнеработает,обратитевниманиенаданныйкомментарий(этонеобязательно,атолькодлятех,ктостолкнулсяспроблемой).Полезныессылки:
connect(офф.документация)
Присоединениеданных(connect)
37
КомбинированиередьюсеровЗачем?Когданашеприложениеразрастается,хочетсяещебольшемодульности,чтобыкаждыйкусочеккодаотвечалзаконкретнуючасть.Такжеисредьюсерами,мыможемразбитьнашглавныйредьюсернанесколькоболеемелких,испомощьюcombineReducersизпакетаreduxсобратьихвоедино.Причем,абсолютноникакоймагии,combineReducersпростовозвращает"составной"редьюсер.
Длянашегоприложения,можновыделитьследующиеreducer'ы(согласносхеме):
userpage
Создадимих:
src/reducers/user.js
constinitialState={
name:'Аноним'
}
exportdefaultfunctionuser(state=initialState){
returnstate
}
src/reducers/page.js
constinitialState={
year:2016,
photos:[]
}
exportdefaultfunctionpage(state=initialState){
returnstate
}
Обновимточкувходадляредьюсеров:
src/reducers/index.js
Комбинированиередьюсеров
38
import{combineReducers}from'redux'
importpagefrom'./page'
importuserfrom'./user'
exportdefaultcombineReducers({
page,
user
})
Обратитевнимание,чтоструктураобъекта,которымможноописатьвсесостояниенашегоприложения-неизменилась.Всеосталосьтакже.
{
user:{
name:'Аноним'
}
page:{
year:2016,
photos:[]
}
}
Темнеменее,вбраузереунаснерабочееприложение.Вчемжепроблема?
ОтветкроетсявработефункцииconnectивфункцииmapStateToPropsизнашегофайлаApp.js.Сейчасунастамнаписаноследующее:
functionmapStateToProps(state){
return{
user:state
}
}
Чтоможноперевеститак:возьмиполностью"стейт"приложенияиприсоединиеговпеременнуюuser,дабыонабыладоступнаизкомпонетаApp.jsкакthis.props.user
Комбинированиередьюсеров
39
Идействительно,есливзглянутьвdevtools,всетакиесть:
Здесь,япредложупростуюзадачкунапониманиепроисходящего.ИзменитекомпонентApp.jsифункциюmapStateToPropsтак,чтобыполучитьследующуюкартину:
Ответ:
src/containers/App.js
Комбинированиередьюсеров
40
importReact,{Component}from'react'
import{connect}from'react-redux'
classAppextendsComponent{
render(){
const{name}=this.props.user//(1)
const{year,photos}=this.props.page//(2)
return<div>
<p>Привет,{name}!</p>
<p>Утебя{photos.length}фотоза{year}год</p>
</div>
}
}
functionmapStateToProps(state){
return{
user:state.user,//(1)
page:state.page//(2)
}
}
exportdefaultconnect(mapStateToProps)(App)
Сносками(1)и(2)япометилсвязь.
Супер,сейчасунасвuser-попадетвсеизнашегоприложения,чтобудетсвязаноспользователем,авpage-попадетвсечтосвязаносотображениемсоответствующегоблока(годимассивфото).
Второйраззаглаву,возникаетвопрос-зачем?Ответпрежний:модульность,меньшийобъемкодавкаждомфайлеилучшаячитаемость.Аглавное,мытакдобьемсяменьшегоколичестваненужныхобновлений,представьте:пользователькликнулнакнопку"2015",иобновилсяблокpage,приэтомблокuserосталсябынетронутымвообще,еслибыонявлялсяотдельнымкомпонентом.
Намничегонемешаетисправитьэто.Продолжимвследующейглаве.
Исходныйкоднатекущиймомент.
Полезныессылки:
combineReducers(офф.документация)
Комбинированиередьюсеров
41
КонтейнерыикомпонентыПреждечеммыразобьемApp.jsнакомпонентыUser.jsиPage.jsхотелосьбыотметитьпроразделениена"компоненты"и"контейнеры",иначеназываемые:"глупые"и"умные"компоненты,"Presentational"и"Container"ибытьможеткак-тоеще.
Позволюсебевочереднойразприбегнутькофф.документациииперевеститаблицуразличий,котораяотличноикраткоотражаетсуть.
Компонент(глупый) Контейнер(умный)
Цель Какэтовыглядит(разметка,стили)
Какэтоработает(получениеданных,обновлениесостояния)
ОсведомленоRedux Нет Да
Длясчитывания
данных
Читаетданныеизprops ПодписаннаReduxstate(состояние)
Дляизменения
данных
Вызываетcallbackизprops
Отправляет(dispatch)Reduxдействие(actions)
Пишутся Вручную Обычно,генерируютсяRedux
Магиятаблицобычнопроявляетсянесразу.Еслипереписатьнашеприложение,апотомвзглянутьсюдаещераз-многоестанетгораздояснее.Предлагаютакипоступить.Поехали!
Создадимкомпоненты.
src/components/User.js
Контейнерыикомпоненты
42
importReact,{PropTypes,Component}from'react'
exportdefaultclassUserextendsComponent{
render(){
const{name}=this.props
return<div>
<p>Привет,{name}!</p>
</div>
}
}
User.propTypes={
name:PropTypes.string.isRequired
}
src/components/Page.js
importReact,{PropTypes,Component}from'react'
exportdefaultclassPageextendsComponent{
render(){
const{year,photos}=this.props
return<div>
<p>Утебя{photos.length}фотоза{year}год</p>
</div>
}
}
Page.propTypes={
year:PropTypes.number.isRequired,
photos:PropTypes.array.isRequired
}
НашфайлApp.jsужепрактическииестьcontainer,ондажележитвсоответствующейпапке.Изменим-с...
src/containers/App.js
Контейнерыикомпоненты
43
importReact,{Component}from'react'
import{connect}from'react-redux'
importUserfrom'../components/User'
importPagefrom'../components/Page'
classAppextendsComponent{
render(){
const{user,page}=this.props
return<div>
<Username={user.name}/>
<Pagephotos={page.photos}year={page.year}/>
</div>
}
}
functionmapStateToProps(state){
return{
user:state.user,
page:state.page
}
}
exportdefaultconnect(mapStateToProps)(App)
ТеперьможнообновлятькомпонентыPageиUserнезависимодруготдруга.Чеммыизаймемсявследующейглаве,изучаяactions.
Контейнерыикомпоненты
44
СозданиеactionsНаконец-томыподходимквопросувзаимодействияспользователемприложения.Практическилюбоедействиепользователявинтерфейсе=отправкадействия(dispatchactions)
Покликунакнопкугода,нашеприложение:
устанавливаетзаголовокзагружаетфотоэтогогода
Сейчаспредлагаюрассмотретьустановкузаголовка.Загрузкафототребуетвыполненияасинхронногозапроса,ачтобыдобратьсядоэтого,мыдолжнырассмотретьнесколькоинтересныхвещей.Ктомуже,установказаголовкаотличнопоказываетнапростомпримере,каквращаютсяданныевнутриredux-приложения,аименно:
1. Приложениеполучилоизначальноесостояние(initialstate)2. Пользовательнажавкнопку,отправилдействие(dispatchaction)3. Соответсвующийредьюсеробновилчастьприложения,всогласиистем,что
узналотдействия.4. Приложениеизменилосьитеперьотражаетновоесостояние.5. ...(всеповторяетсяпокругу,спункта2)
Этоиестьоднонаправленныйпотокданных.
Создадимpageaction:
src/actions/PageActions.js
exportfunctionsetYear(year){
return{
type:'SET_YEAR',
payload:year
}
}
Напоминаю,чтополяtypeиpayload-всеголишь"негласное"соглашение.Немногообэтом,можнопочитатьнаанглийскомтут.
Созданиеactions
45
Поправимредьюсерpage:
src/reducers/page.js
constinitialState={
year:2016,
photos:[]
}
exportdefaultfunctionpage(state=initialState,action){
switch(action.type){
case'SET_YEAR':
return{...state,year:action.payload}
default:
returnstate;
}
}
Обратитевнимание,варгументахуфункцииpageуказанвторойаргумент-action.Этостандартныеаргументыreduxreducer'а.Благодаряэтому,мыможемлегкообрабатыватьразличныедействияпоихтипу,попадаявнужнуюсекциюcaseоператораswitch.
Такжеобратитевнимание,чтомынеизменилиобъектstate,авернулиновыйсполемyearравнымaction.payload(азначитгодом,выбраннымпользователем).
Добавляемвызовactionsизкомпонентов
Унасестьaction,иестьreducerготовыйизменитьstateприложения(да,янарочнопишуиногдаэтисловапо-английски).Нонашкомпонентнезнаеткакобратитьсякнеобходимомудействию.
Согласнотаблицеизпрошлогораздела:дляизмененияданных,нашкомпонентPage.js,долженвызыватьcallbackизthis.props,анашконтейнер*App.js-отправлятьдействие(dispatchaction).
*яговорю,контейнер,хотяправильнееназыватьконтейнером<Connect(App)/>,нотаккаконгенерируетсяфункциейconnectнаосновеApp.js,считаюэтодопустимым.
Издокументациифункцииconnect,намтакжестановитсяясно,чтоспомощьюэтойфункциимыможемнетолькоподписатьсянаобновленияданных,нои"прокинуть"нашиactionsвконтейнер.
Созданиеactions
46
connect,первымаргументомпринимает"маппинг"(соответствие)stateкprops,авторыммаппингdispatchкprops.Какбыдикоэтонезвучало,напрактикеэтозначит,чтонамдостаточнопередатьвторойаргумент.
ИсправимApp.js
src/containers/App.js
importReact,{Component}from'react'
import{bindActionCreators}from'redux'
import{connect}from'react-redux'
importUserfrom'../components/User'
importPagefrom'../components/Page'
import*aspageActionsfrom'../actions/PageActions'
classAppextendsComponent{
render(){
const{user,page}=this.props
const{setYear}=this.props.pageActions
return<div>
<Username={user.name}/>
<Pagephotos={page.photos}year={page.year}setYear={setYear}/>
</div>
}
}
functionmapStateToProps(state){
return{
user:state.user,
page:state.page
}
}
functionmapDispatchToProps(dispatch){
return{
pageActions:bindActionCreators(pageActions,dispatch)
}
}
exportdefaultconnect(mapStateToProps,mapDispatchToProps)(App)
НачнемсразбораmapDispatchToProps.Внутрифункциимыиспользоваливспомогательнуюфункциюизredux-bindActionCreators(офф.документация,котораяпозволилавызыватьsetYear,есливыразитьсяпростоснекоторымидопущениямикак:
Созданиеactions
47
store.dispatch({
type:'SET_YEAR'
payload:2016
})
Темсамымнеобходимоеизменениепрослушиваетсявreduxstore,ивнашемредьюсереPageсоответственно.
Следовательно,послевыполненияconnect(mapStateToProps,mapDispatchToProps)(App),мыполучиливApp.jsновыесвойства(props),чтонагляднодемонстрируетвкладка"React"вchromedevtools.
ДобавивsetYearвсвойстваPage.js,несоставиттрудаиспользоватьнеобходимыйactionизкомпонента,которыйпопрежнемузнатьничегонезнаетоredux.
UPDATE[18.03.16]:свойствоinnerTextприведенноевкодениже-нестандартное,поэтомусниммогутвозникнутьпроблемывнекоторыхбраузерах.Вместонего,выможетеиспользовать-textContent.
src/components/Page.js
Созданиеactions
48
importReact,{PropTypes,Component}from'react'
exportdefaultclassPageextendsComponent{
onYearBtnClick(e){
this.props.setYear(+e.target.innerText)
}
render(){
const{year,photos}=this.props
return<div>
<p>
<buttononClick={::this.onYearBtnClick}>2016</button>
<buttononClick={::this.onYearBtnClick}>2015</button>
<buttononClick={::this.onYearBtnClick}>2014</button>
</p>
<h3>{year}год</h3>
<p>Утебя{photos.length}фото.</p>
</div>
}
}
Page.propTypes={
year:PropTypes.number.isRequired,
photos:PropTypes.array.isRequired,
setYear:PropTypes.func.isRequired
}
Собственно,кодкомпонентаPageпопрежнемуоченьпростой.Строка::this.onYearBtnClick===this.onYearBtnClick.bind(this),инужнатаккакReactсверсии0.14.xнепривязываетthisккомпоненту.
Использованиедвойногодвоеточия-этовозможностьES7(experimental),котораядоступнавbabelснастройкойstage=0(длятехктописалкод,начинаясраздела"Подготовка"-всеуженастроено,смотрифайл.babelrc)
Глававыдаласьдостаточнодлинной,ахужевсего,чтомынаписали"кипу"кода,всеголишьдляобновленияцифрывзаголовке.Гдепрофит,какговорится?
Профитобнаружитсядальше,когдавашеприложениеразрастется.Когдаегобудетнеобходимоподдерживатьидобавлятьновыефичи.Засчетоднонаправленногопотокаданных(юзеркликнул-действиевызвалось-редьюсеризменилсостояние-компонентотрисовализменения)дажевприложении,написанномдавно,увасполучитсяоченьбыстроразобратьсяивнестинеобходимыеобновления,которыетребуетбизнес.Ктомуже,такойподходотличноработаетидлякоманднойработы.
Созданиеactions
49
КонстантыЕсливынестивсеpageactionsвотдельныйфайлсконстантами,товбудущемнамудобнеебудетписатьтесты/работатьвкоманде/поддерживатькод.Ктомуже,такимобразоммынебудемотходитьот"соглашений"принятыхвразработкеFlux/Reduxприложений.
src/constants/Page.js
exportconstSET_YEAR='SET_YEAR'
ПодключимконстантувредьюсерPageивPageActions
src/reducers/page.js
import{SET_YEAR}from'../constants/Page'
constinitialState={
year:2016,
photos:[]
}
exportdefaultfunctionpage(state=initialState,action){
switch(action.type){
caseSET_YEAR://незабудьтеобновитьстрокунаконстанту
return{...state,year:action.payload}
default:
returnstate;
}
}
src/actions/PageActions.js
Константы
51
import{SET_YEAR}from'../constants/Page'
exportfunctionsetYear(year){
return{
type:SET_YEAR,//аналогично,теперьиспользуемконстанту
payload:year
}
}
Вдальнейшеммыещедобавимконстант,нетолькодлякомпонента<Page/>,ноидлякомпонента<User/>,которыемытакжебудемобъявлятьвотдельномфайле.
Константы
52
НаводимпорядокДаннаяглаваявляетсясвоегорода"перекуром".Оназатрагиваетвопросыстилейиверсткиприложения.
Autoprefixerистили
Добавимвwebpackвозможностьобрабатыватьстили,заодносразунакинувнанихвозможностиautoprefixer'а.
npminstallstyle-loadercss-loaderpostcss-loaderautoprefixerprecss--save-dev
P.S.длятехктопользуетсяавтоматическимспособомдобавлениязависимостей:таккакмыдобавляемзависимостивwebpack.config-npm-insatll-pluginнеможетподтянутьихавтоматически.
webpack.config.js
varpath=require('path')
varwebpack=require('webpack')
varNpmInstallPlugin=require('npm-install-webpack-plugin')
varautoprefixer=require('autoprefixer');
varprecss=require('precss');
module.exports={
devtool:'cheap-module-eval-source-map',
entry:[
'webpack-hot-middleware/client',
'babel-polyfill',
'./src/index'
],
output:{
path:path.join(__dirname,'dist'),
filename:'bundle.js',
publicPath:'/static/'
},
plugins:[
newwebpack.optimize.OccurenceOrderPlugin(),
newwebpack.HotModuleReplacementPlugin(),
newNpmInstallPlugin()
],
module:{
preLoaders:[
Наводимпорядок
53
{
test:/\.js$/,
loaders:['eslint'],
include:[
path.resolve(__dirname,"src"),
],
}
],
loaders:[
{
loaders:['react-hot','babel-loader'],
include:[
path.resolve(__dirname,"src"),
],
test:/\.js$/,
plugins:['transform-runtime'],
},
{
test:/\.css$/,
loader:"style-loader!css-loader!postcss-loader"
}
]
},
postcss:function(){
return[autoprefixer,precss];
}
}
Незабудьтеподключитьглавныйфайлстилейдлявсегоприложения:
src/index.js
importReactfrom'react'
import{render}from'react-dom'
import{Provider}from'react-redux'
importAppfrom'./containers/App'
import'./styles/app.css'//<--импортстилей
importconfigureStorefrom'./store/configureStore'
conststore=configureStore()
render(
<Providerstore={store}>
<divclassName='app'>{/*обернуливсев.app*/}
<App/>
</div>
</Provider>,
document.getElementById('root')
)
Наводимпорядок
54
Возможно(есливыкопируетекод,либотожелюбитеодинарныекавычки),wepbackвыдастошибкуотESLint.Дляиспользованияодинарнойкавычки,исправьтеправило"jsx-quotes":[1,"prefer-single"]вфайлике.eslintrc
Верстка
Версткаистилинеявляютсятемойнашегообучения,поэтомуможетепростовзглянутьнаисходныйкод,либосделатькаквамхочется.
Вреальномприложении,имеетсмыслстилидлякомпонентовимпортироватьвкодесамихкомпонентов,чтодасточеньбольшиеудобствадляпереиспользованияцелыхблоков,включаяоформление.
Наводимпорядок
55
Middleware(Усилители).ЛоггерПреждечеммысможемсоздаватьасинхронныедействия,поговоримобусилителяхинапишем,обещанныйранееусилитель-логгер.
Представляйтеусилитель,какнечтостороннее,добавляющеефункционалдлянашегоstore.
Усилители,этоmiddleware.Сутьmiddlewareфункций,взятьвходныеданные,добавитьчто-тоипередатьдальше.
Например:естьконвейер,покоторомудвижетсяпальто.НаконвейереработаютЗинаиЛюдмила.Зинапришиваетпуговку,Людмилаприкладываетбирку.Внезапно,появляетсяmiddlewareЛена,встаетмеждуЗинойиЛюдмилойикраситпуговкувхипстерскиймодныйцвет.ТаккакЛенапослепокраскинеуноситпальтоссобой,апередаетдальше,тоЛюдмилакакнивчемнебывалоприделываетбиркуипальтоготово.Толькотеперьонохипстерское.Усиленное.
Длялучшегопонимания,предлагаюнаписатьбесполезныйусилитель,выдающийconsole.log('ping'),накаждоедействие.Приэтом,мыбудемиспользоватьпредложенныйreduxметоддобавленияусилитейспомощьюapplyMiddleware.
Обновимфайлконфигурацииstore:
store/configureStore.js
Middleware(усилители).Логгер
56
import{createStore,applyMiddleware}from'redux'
importrootReducerfrom'../reducers'
import{ping}from'./enhancers/ping'//<!--подключаемнашenhancer
exportdefaultfunctionconfigureStore(initialState){
conststore=createStore(
rootReducer,
initialState,
applyMiddleware(ping))//<!--добавляемеговцепочкуmiddleware'ов
if(module.hot){
module.hot.accept('../reducers',()=>{
constnextRootReducer=require('../reducers')
store.replaceReducer(nextRootReducer)
})
}
returnstore
}
Напишемусилитель:
store/enhancers/ping.js
/*eslint-disable*/
exportconstping=store=>next=>action=>{
console.log('ping')
returnnext(action)
}
/*eslint-enable*/
Боюсь,здесьнеобойтисьбезES5версии:
varping=functionping(store){
returnfunction(next){
returnfunction(action){
console.log('ping');
returnnext(action);
};
};
};
Поехали:
eslint-disable-простовыключаетпроверкуэтогоблока"линтером".ping-этофункция,котораявозвращаетфункцию.Middleware-этовсегдафункция,которыеобычновозвращаютфункцию,еслитолькоцельюmiddlewareнеявляется
Middleware(усилители).Логгер
57
прерватьцепочкувызовов.ввозвращаемыхфункциях,благодаряapplyMiddlewareунасстановятсядоступнымиаргументы,которыемыможемиспользоватьвоблагоприложения:
store-redux-storeнашегоприложения;next-функция-обертка,котораяпозволяетпродолжитьвыполнениецепочки;action-действие,котороебыловызвано(каквыпомните,вызванныедействия-этоstore.dispatch)
Сейчас,прикликенакнопки,унасвконсолипоявляетсястрокаping.Давайтеизменимее,написавпростейшийлоггер:store/enhancers/ping.js
/*eslint-disable*/
exportconstping=store=>next=>action=>{
console.log(`Типсобытия:${action.type},дополнительныеданныесобытия:${action.pa
yload}`)
returnnext(action)
}
/*eslint-enable*/
Яиспользовалновыйстроковыйсинтаксис.Впрошлом,нашconsole.logвыгляделбытак:
console.log('Типсобытия:'+action.type+',дополнительныеданныесобытия:'+acti
on.payload)
Middleware(усилители).Логгер
58
Покликайтенакнопки,результатдолженбытьследующим:
Redux-logger
Отбросимнашвелосипедипоставимпопулярныйлоггер.
npmi--saveredux-logger
Удалитепапкуenchancers,иизменитеconfigureStore.
src/store/configureStore.js
Middleware(усилители).Логгер
59
import{createStore,applyMiddleware}from'redux'
importrootReducerfrom'../reducers'
importcreateLoggerfrom'redux-logger'
exportdefaultfunctionconfigureStore(initialState){
constlogger=createLogger()
conststore=createStore(
rootReducer,
initialState,
applyMiddleware(logger))
if(module.hot){
module.hot.accept('../reducers',()=>{
constnextRootReducer=require('../reducers')
store.replaceReducer(nextRootReducer)
})
}
returnstore
}
Можетепроверить-логгердостаточноинформативныйиудобенвиспользовании.
Такимобразом,усилители-отличныйспособдобавитьвнашпроцессобработкидействийнекуюпрослойкуснеобходимойфункциональностью.
Однимизпопулярнейшихусилителей,являетсяredux-thunk,которыймыкакразбудемиспользоватьдлясозданияасинхронныхдействий.
Исходныйкоднатекущиймомент.
Middleware(усилители).Логгер
60
АсинхронныеactionsДавайтепредставимсинхронноедействие:
1. Пользователькликнулнакнопку2. dispatchaction{type:ТИП_ДЕЙСТВИЯ,payload:доп.данные}3. интерфейсобновился
Давайтепредставимасинхронноедействие:
1. Пользователькликнулнакнопку2. dispatchaction{type:ТИП_ДЕЙСТВИЯ_ЗАПРОС}3. запросвыполнилсяуспешно
i. dispatchaction{type:ТИП_ДЕЙСТВИЯ_УСПЕШНО,payload:доп.данные}4. запросвыполнилсянеудачно
i. dispatchaction{type:ТИП_ДЕЙСТВИЯ_НЕУДАЧНО,error:true,payload:доп.данныеошибки}
Благодарятакойсхеме,вreducer'eмысможемреализоватьподобное:
switch(тип_действия)
caseТИП_ДЕЙСТВИЯ_ЗАПРОС:
покажиpreloader
caseТИП_ДЕЙСТВИЯ_УСПЕШНО:
скройpreloader,покажиданные
caseТИП_ДЕЙСТВИЯ_НЕУДАЧНО:
скройpreloader,покажиошибку
Какнамизвестно,действие-этопростойобъект,которыйвозвращаетсяфункциейегосоздающей(actioncreator).
Убедимсявэтом:
src/actions/PageActions.js
import{SET_YEAR}from'../constants/Page'
exportfunctionsetYear(year){
return{
type:SET_YEAR,
payload:year
}
}
Асинхронныеactions
61
Былобынеплохоиметьвозможностьвозвращатьнепростойобъект,афункцию,внутрикоторойиметьдоступкметодуdispatch,ивызыватьегоснеобходимымтипомдействия.Псевдокод,могбывыглядетьтак:
exportfunctiongetPhotos(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-этопростойобъект,иеслиactioncreatorвозвращаетнепростойобъект,афункцию,тоэтокак-то...Подождите!Ведьэтоименното,чтонамнужно:Еслиactioncreatorвозвращаетнепростойобъект,афункцию-выполниее,иначееслиэтопростойобъект...тадам,передайдальше.Болеетого,благодаряapplyMiddlewareунаскакразестьдоступныйметодdispatch!ИещебонусомgetState.
Отлично,мытолькочтопоняли,чтонамнуженещеодинусилитель.Такойусилительуженаписан,причемкодегоневероятнопрост,ядажеприведуегоздесь:
усилитель:redux-thunk
functionthunkMiddleware({dispatch,getState}){
returnnext=>action=>
typeofaction==='function'?
action(dispatch,getState):
next(action);
}
module.exports=thunkMiddleware
Асинхронныеactions
62
Намостаетсялишьдобавитьзависимостьвнашпроект,иубедиться,чтоунасreduxверсии,нениже3.1.0
npmupdateredux--save
npminstallredux-thunk--save
Дляпрактики,предлагаюнаписатьследующее:
покликунакнопкусномеромгодаменяетсягодвзаголовкениже(гдедолжныбытьфото),появляетсятекст"Загрузка..."
послеудачнойзагрузки*убратьтекст"Загрузка..."отобразитьстроку"УтебяХХфото"(зависит,отдлинымассива,переданноговaction.payload)
*вместореальногометодазагрузки,использоватьsetTimeout,которыйявляетсяудобнымдлятренировокисполненияасинхронныхзапросов.
Выможетепопробоватьвыполнитьэтозаданиесами,апотомсравнитьегосрешениемниже.
Дляотображения/скрытияфразы"Загрузка...",используйтевreducer'еещеодносвойствоусостояния.Например,fetching:
constinitialState={
year:2016,
photos:[],
fetching:false
}
Решениениже.
Дляначалаизменимнаборконстант:
src/constants/Page.js
exportconstGET_PHOTOS_REQUEST='GET_PHOTOS_REQUEST'
exportconstGET_PHOTOS_SUCCESS='GET_PHOTOS_SUCCESS'
Далеедобавимновыйусилитель:src/store/configureStore.js
Асинхронныеactions
63
import{createStore,applyMiddleware}from'redux'
importrootReducerfrom'../reducers'
importcreateLoggerfrom'redux-logger'
importthunkfrom'redux-thunk'//<--добавилиredux-thunk
exportdefaultfunctionconfigureStore(initialState){
constlogger=createLogger()
conststore=createStore(
rootReducer,
initialState,
applyMiddleware(thunk,logger))//<--добавилиеговцепочкупередlogger'ом
if(module.hot){
module.hot.accept('../reducers',()=>{
constnextRootReducer=require('../reducers')
store.replaceReducer(nextRootReducer)
})
}
returnstore
}
Изменимactioncreator:src/actions/PageActions.js
import{
GET_PHOTOS_REQUEST,
GET_PHOTOS_SUCCESS
}from'../constants/Page'
exportfunctiongetPhotos(year){
return(dispatch)=>{
dispatch({
type:GET_PHOTOS_REQUEST,
payload:year
})
setTimeout(()=>{
dispatch({
type:GET_PHOTOS_SUCCESS,
payload:[1,2,3,4,5]
})
},1000)
}
}
Изменимreducer:src/reducers/page.js
Асинхронныеactions
64
import{
GET_PHOTOS_REQUEST,
GET_PHOTOS_SUCCESS
}from'../constants/Page'
constinitialState={
year:2016,
photos:[],
fetching:false
}
exportdefaultfunctionpage(state=initialState,action){
switch(action.type){
caseGET_PHOTOS_REQUEST:
return{...state,year:action.payload,fetching:true}
caseGET_PHOTOS_SUCCESS:
return{...state,photos:action.payload,fetching:false}
default:
returnstate;
}
}
Унасготовалогикадляобновлениясостояния(иинтерфейса,разумеется).Осталосьпоправитьотображение.
Таккакмыпереписалиипереименовалифункцию(setYear->getPhotos):
src/containers/App.js
...
const{getPhotos}=this.props.pageActions
return<divclassName='row'>
<Pagephotos={page.photos}year={page.year}getPhotos={getPhotos}fetching={page.
fetching}/>
...
Причем,вmapDispatchToProps-намничегоменятьненужно,таккакмыпопрежнемуприсоединяемвсеpageActionsвpropsконтейнера<App/>
Обновимсоответствующийкомпонент:src/components/Page.js
Асинхронныеactions
65
importReact,{PropTypes,Component}from'react'
exportdefaultclassPageextendsComponent{
onYearBtnClick(e){
this.props.getPhotos(+e.target.innerText)
}
render(){
const{year,photos,fetching}=this.props
return<divclassName='ibpage'>
<p>
<buttonclassName='btn'onClick={::this.onYearBtnClick}>2016</button>{''}
<buttonclassName='btn'onClick={::this.onYearBtnClick}>2015</button>{''}
<buttonclassName='btn'onClick={::this.onYearBtnClick}>2014</button>
</p>
<h3>{year}год</h3>
{
fetching?
<p>Загрузка...</p>
:
<p>Утебя{photos.length}фото.</p>
}
</div>
}
}
Page.propTypes={
year:PropTypes.number.isRequired,
photos:PropTypes.array.isRequired,
getPhotos:PropTypes.func.isRequired
}
Когдабудетепроверятьработувбраузере,обратитевниманиеналоггер.Онвсетакжеработаетиинформативен.
Покамыписаликоддляасинхронногозапроса,мыНЕнарушилиглавныепринципыredux-приложения:
1. Мывсегдавозвращалиновоесостояние(новыйобъект,смотритеsrc/reducers/page.js)
2. Мыстрогоследовалиоднонаправленномупотокуданныхвприложении:юзеркликнул-возниклодействие-редьюсеризменил-компонентотобразил.
Итого:выможетесамидописатьнашеприложение,чтобыоновзаимодействовалосVK,таккаквсечтонужно,этодобавитьреальныйасинхронныйзапрос(точнеепарочку-длялогина,идляполученияфото).Ложкудегтядобавляеттотфакт,чтодляэтого
Асинхронныеactions
66
потребуетсясоздатьвинтерфейсеVKприложение,ивыполнятьнашизапросысреальногосервера,таккакVK.APIнеработаетсlocalhost.
Обэтоммыипоговоримвследующейглаве.
Исходныйкоднаданныймомент.
Асинхронныеactions
67
ВзаимодействуемсVKЧтобыработатьсVKAPIвамнеобходимобудетсоздатьприложениенасайтеvk.com,иуказатьвнастройкахURLсервера,скотороговыбудетевыполнятьзапросы.
Localhostнеподдерживается.
ИнтеграцияVKAPI
Необходимодобавитьскриптopenapiпереднашейсборкой-bundle.js,атакжевызватьVK.init
<!DOCTYPEhtml>
<html>
<head>
<title>Redux[RU]Tutorial</title>
</head>
<body>
<divid="root"class="container-fluid">
</div>
<scriptsrc="//vk.com/js/api/openapi.js"></script>
<scriptsrc="/static/bundle.js"></script>
<scriptlanguage="javascript">
VK.init({
apiId:5087365
});
</script>
</body>
</html>
Авторизация
СоздадимдействиядляUser.
src/actions/UserActions.js
ВзаимодействуемсVK
68
import{
LOGIN_REQUEST,
LOGIN_SUCCES,
LOGIN_FAIL
}from'../constants/User'
exportfunctionhandleLogin(){
returnfunction(dispatch){
dispatch({
type:LOGIN_REQUEST
})
VK.Auth.login((r)=>{//eslint-disable-lineno-undef
if(r.session){
letusername=r.session.user.first_name;
dispatch({
type:LOGIN_SUCCES,
payload:username
})
}else{
dispatch({
type:LOGIN_FAIL,
error:true,
payload:newError('Ошибкаавторизации')
})
}
},4);//запросправнадоступкphoto
}
}
Проверьтесписокконстант:
exportconstLOGIN_REQUEST='LOGIN_REQUEST'
exportconstLOGIN_SUCCES='LOGIN_SUCCES'
exportconstLOGIN_FAIL='LOGIN_FAIL'
"Приконнектим"в<App/>UserActions,идобавимновыесвойствавкомпонент<User/>
src/containers/App.js
ВзаимодействуемсVK
69
importReact,{Component}from'react'
import{bindActionCreators}from'redux'
import{connect}from'react-redux'
importUserfrom'../components/User'
importPagefrom'../components/Page'
import*aspageActionsfrom'../actions/PageActions'
import*asuserActionsfrom'../actions/UserActions'
classAppextendsComponent{
render(){
const{user,page}=this.props
const{getPhotos}=this.props.pageActions
const{handleLogin}=this.props.userActions
return<divclassName='row'>
<Pagephotos={page.photos}year={page.year}getPhotos={getPhotos}fetching={page.
fetching}/>
<Username={user.name}handleLogin={handleLogin}error={user.error}/>
</div>
}
}
functionmapStateToProps(state){
return{
user:state.user,
page:state.page
}
}
functionmapDispatchToProps(dispatch){
return{
pageActions:bindActionCreators(pageActions,dispatch),
userActions:bindActionCreators(userActions,dispatch)
}
}
exportdefaultconnect(mapStateToProps,mapDispatchToProps)(App)
Обновимreduceruser:
src/reducers/user.js
ВзаимодействуемсVK
70
import{
LOGIN_SUCCES,
LOGIN_FAIL
}from'../constants/User'
constinitialState={
name:'',
error:''
}
exportdefaultfunctionuser(state=initialState,action){
switch(action.type){
caseLOGIN_SUCCES:
return{...state,name:action.payload,error:''}
caseLOGIN_FAIL:
return{...state,error:action.payload.message}
default:
returnstate
}
}
Ипокажемвсеэтовкомпоненте<User/>
src/components/User.js
ВзаимодействуемсVK
71
importReact,{PropTypes,Component}from'react'
exportdefaultclassUserextendsComponent{
render(){
const{name,error}=this.props
lettemplate
if(name){
template=<p>Привет,{name}!</p>
}else{
template=<buttonclassName='btn'onClick={this.props.handleLogin}>Войти</button
>
}
return<divclassName='ibuser'>
{template}
{error?<pclassName='error'>{error}.<br/>Попробуйтеещераз.</p>:''}
</div>
}
}
User.propTypes={
name:PropTypes.string.isRequired,
handleLogin:PropTypes.func.isRequired,
error:PropTypes.string.isRequired
}
Сейчасесликликнутьна"войти"-всплыветVKокносподтверждениемправдоступа(первыйраз).Послеподтвержденияправ,вместокнопкивойтипоявляетсянадпись"Привет,ХХХ".Приперезагрузкесайтаиповторныхнажатияхна"войти"-VKокномгновеннозакрывается,акнопкавновьизменяетсяна"Привет,XXX".Неплохобыбылопроверять"статус",напримервcomponentWillMount,нооставлюэтона"домашку".
Каквсегда,доблестныйлоггерпишетвконсоли-чтопроисходит.
Загрузкафото
Намнужнопрактическиповторить,всечтонаписановыше,толькодляблокаPage.
Можетепопробоватьсами,используяметодphotos.getAllизVKAPI.
Дляначала,проверимсписокконстант:
ВзаимодействуемсVK
72
src/constants/Page.js
exportconstGET_PHOTOS_REQUEST='GET_PHOTOS_REQUEST'
exportconstGET_PHOTOS_SUCCESS='GET_PHOTOS_SUCCESS'
exportconstGET_PHOTOS_FAIL='GET_PHOTOS_FAIL'
Напишемнемалокода,длязагрузкифото:
src/actions/PageActions.js
import{
GET_PHOTOS_REQUEST,
GET_PHOTOS_FAIL,
GET_PHOTOS_SUCCESS
}from'../constants/Page'
letphotosArr=[]
letcached=false
functionmakeYearPhotos(photos,selectedYear){
letcreatedYear,yearPhotos=[]
photos.forEach((item)=>{
createdYear=newDate(item.created*1000).getFullYear()
if(createdYear===selectedYear){
yearPhotos.push(item)
}
})
yearPhotos.sort((a,b)=>b.likes.count-a.likes.count);
returnyearPhotos
}
functiongetMorePhotos(offset,count,year,dispatch){
VK.Api.call('photos.getAll',{extended:1,count:count,offset:offset},(r)=>{//
eslint-disable-lineno-undef
try{
if(offset<=r.response[0]-count){
offset+=200;
photosArr=photosArr.concat(r.response)
getMorePhotos(offset,count,year,dispatch)
}else{
letphotos=makeYearPhotos(photosArr,year)
cached=true
dispatch({
type:GET_PHOTOS_SUCCESS,
payload:photos
})
}
}
ВзаимодействуемсVK
73
catch(e){
dispatch({
type:GET_PHOTOS_FAIL,
error:true,
payload:newError(e)
})
}
})
}
exportfunctiongetPhotos(year){
return(dispatch)=>{
dispatch({
type:GET_PHOTOS_REQUEST,
payload:year
})
if(cached){
letphotos=makeYearPhotos(photosArr,year)
dispatch({
type:GET_PHOTOS_SUCCESS,
payload:photos
})
}else{
getMorePhotos(0,200,year,dispatch)
}
}
}
makeYearPhotosиgetMorePhotosможновынестивпапкуutils,каквспомогательныефункции.
Главноездесь,чтомыпопрежнемувызываемдействия(dispatchactions).Всетак,какбыловсамомначале,простодобавилосьнемногобольшелогикидляполученияфото.Алгоритмполучениявсехфото(даинеобходимостьполучениявсех)-оставляюбезкомментариев.Мнекажется,этоприемлемыйспособ.
Чтобыпотестироватьпоказошибок,достаточнопростоисправитьцифру200на2или20.VKслюбовьювамответит,чтовымягко-говоря,оченьнастойчивообращаетиськAPI;)
Исправивредьюсериотрисовкувкомпоненте,мызакончимначатое.
src/reducers/page.js
ВзаимодействуемсVK
74
import{
GET_PHOTOS_REQUEST,
GET_PHOTOS_SUCCESS,
GET_PHOTOS_FAIL
}from'../constants/Page'
constinitialState={
year:2016,
photos:[],
fetching:false,
error:''
}
exportdefaultfunctionpage(state=initialState,action){
switch(action.type){
caseGET_PHOTOS_REQUEST:
return{...state,year:action.payload,fetching:true,error:''}
caseGET_PHOTOS_SUCCESS:
return{...state,photos:action.payload,fetching:false,error:''}
caseGET_PHOTOS_FAIL:
return{...state,error:action.payload.message,fetching:false}
default:
returnstate;
}
}
src/components/Page.js
ВзаимодействуемсVK
75
importReact,{PropTypes,Component}from'react'
exportdefaultclassPageextendsComponent{
onYearBtnClick(e){
this.props.getPhotos(+e.target.innerText)
}
render(){
const{year,photos,fetching,error}=this.props
constyears=[2016,2015,2014,2013,2012,2011,2010]
return<divclassName='ibpage'>
<p>
{years.map((item,index)=><buttonclassName='btn'key={index}onClick={::th
is.onYearBtnClick}>{item}</button>)}
</p>
<h3>{year}год[{photos.length}]</h3>
{error?<pclassName='error'>Вовремязагрузкифотопроизошлаошибка</p>:''
}
{
fetching?
<p>Загрузка...</p>
:
photos.map((entry,index)=>
<divkey={index}className='photo'>
<p><imgsrc={entry.src}/></p>
<p>{entry.likes.count}❤</p></div>
)
}
</div>
}
}
Page.propTypes={
year:PropTypes.number.isRequired,
photos:PropTypes.array.isRequired,
getPhotos:PropTypes.func.isRequired,
error:PropTypes.string.isRequired
}
Итого:Вынаучилисьвыполнятьасинхронныезапросыикорректнопоказыватьпрелоадер,ошибкиилиуспешныйрезультат.
Исходныйкоднатекущиймомент.
P.S.cssтожебылслегкаподправлен.
ВзаимодействуемсVK
76
ВзаимодействуемсVK
77
ЗаключениеСпасибо,чтовыпрочиталиданныйучебник,еслиувасосталисьвопросы,задавайтеихвтвиттере,илипишитенапочту[email protected]стемой"ReduxRU".
Планобновлений:
разделпоосновамreact.js->написантуториалтестированиеоптимизациясборки
Какговорится,staytuned!
Заключение
78