Всем привет! Сегодня у нас будет довольно важный урок, мы все ближе и ближе подбираемся к Redux, но для начала пробежимся по нашему домашнему заданию, оно было довольно простым, но все таки для проверки покажу как добавить наш календарь.
Установим наш модуль:
npm install react-day-picker —s
Проверим, что в нашем package.json появилась запись об этом:
"react-day-picker": "^2.3.3",
В ArticleList сделаем import самого Day-picker:
import DayPicker, { DateUtils } from "react-day-picker" import 'react-day-picker/lib/style.css'
State изменим следующим образом:
state = { selectedArticles: null, from: null, to: null }
В наш div добавим к нашему select новый модуль:
<div> <h1>Article list</h1> {this.getRangeTitle()} <Select options = {options} multi = {true} value = {this.state.selectedArticles} onChange = {this.handleSelectChange} /> <DayPicker ref="daypicker" selectedDays={day => DateUtils.isDayInRange(day, this.state)} onDayClick={this.handleDayClick} /> <ul> {listItems} </ul> </div>
А ниже опишем логику работы компонента:
getRangeTitle() { const { from, to } = this.state const fromText = from && `Start date: ${from.toDateString()}` const toText = to && `Finish date: ${to.toDateString()}` return <p>{fromText} {toText}</p> } handleDayClick = (e, day) => { console.log('---', day) const range = DateUtils.addDayToRange(day, this.state); this.setState(range) }
Теперь у нас есть функционирующий календарь и при выборе периода он отображает дату рядом с нашим заголовком “Article list”.
Обратите внимание, что наш компонент ArticleList разрастается, это довольно плохо. У нас появилось довольно много кода – это верный знак к тому чтобы вынести наши select, day-picker вместе с их логикой в отдельные компоненты, скажем в Filters. Проблема в том что если мы сделаем это прямо сейчас то у нас будет много проблем с передачей значений. Нам хотелось бы чтобы наши компоненты были атомарными, выполняющими отдельные узкопрофильные задачи. Можно сказать что наш компонент стал взрослым и нам пора задуматься о бизнес-логике, и добавить Redux или Flux.
Вспомним о том как работает React и его Virtual DOM, в картину его с трудом вписывается MVC подход и т.п., тем более в больших приложениях понять как работает тот или иной компонент достаточно сложно, так как приходиться держать очень много всего в голове (касаемо того как работает приложение), вспомните о Two-way data binding и прочих особенностях Angular и Ember.
Вместо этого команда React предложило другое решение по построении логики – Unidirectional data flow. Рассмотрим каким образом работает Redux.
У нас есть Store – они отвечают за хранение данных, именно из них View читает данные и отображает их.
Action – это events, объекты которые описывают то, что будет происходить. Они создаются для описания коммуникаций с User (клик мышкой, нажатие клавиши) или API.
Dispatcher – Actions попадают в dispatcher, он разбрасывает их по Stores, те в свою очередь реагируют, обрабатываю Actions. Например пришел action deleteArticel, store понимает, что нужно пойти и удалить какую-то статью, и оповещает View о том, что изменилось, View запрашивает все данные которые ей нужны чтобы перестроить UI.
Несколько слов об отличии Flux от Redux:
1. Во-первых Redux больше сосредоточен на концепциях функционального программирования, и все элементы Unidirectional data flow значительно сильные разделены между собой. Скажем Store это Immutubable object который отвечает исключительно за сохранение состояния.
2. В Redux отказались от внешнего Dispatcher’а который связывал все вместе. Также у нас один store на всех, а dispatcher спрятан внутри, для работы с ним разработчикам предоставлен API.
store.dispatch({ type: INCREMENT, data: {amount} }) store.subscribe(callback)
Так как у нас один store у нас один метод подписки на него. Flux очень гибкий и имеет много store – что скорее всего будет мешать при разработке.
3. Action-creators в Redux это просто чистые функции которые возвращают вам объект, с какими либо данными, которые вместе с этим action.
function increment (amount) { return { type: INCREMENT, data: {amount} } }
Диспетчер здесь отсутствует.
4. Stores => Reducers, это механизм перехода между состояниями. Reducers это тоже чистые функции. Это функция которая знает начальное состояние, получив какой то action должна перейти в другое состояние. Они не меняют старое, а возвращают новое:
function counter (number = o, action) { const {type, data} = action return type == INCREMENT ? number + data.amount : number }
Давайте прежде чем делать что-либо сложное сделаем простой Counter с Redux. Будем отображать на странице число и увеличивать его значение при нажатии на кнопку.
Зайдем в app.js и закомментируем наш Articlelist и заведем компонент Counter в папке components:
import React, { Component, PropTypes } from 'react' class Counter extends Component { static propTypes = { count: PropTypes.number }; render() { return ( <div> <h1>{this.props.count}</h1> <a href="#" onClick = {this.handleIncrement}>increment</a> </div> ) } handleIncrement = (ev) => { ev.preventDefault() console.log('---', 'incrementing') } } export default Counter
Также сделаем возможность инкремента по клику. Далее вернемся в app.js и сделаем импорт компонента:
import Counter from './components/Counter'
А также добавим:
render(<Counter count = {0} />, document.getElementById('container'))
Теперь установим Redux в console:
npm i redux –S
Теперь count мы будем хранить в store а не передавать его вручную.
(<Counter count = {0} />
Для этого конечно же создадим Store. Создайте директорию с таким названием в src. Там создадим файл index.js. Для начала обратимся к документации Redux.
Прежде чем говорить про store, прочтите главу createStore. Все ваши данные будут жить в нем. Создается он с помощью функции:
createStore(reducer, [preloadedState], [enhancer])
с одним обязательным аргументом – reducer.
Теперь создадим наш store:
import { createStore } from 'redux' import reducer from '../reducer' const store = createStore(reducer, 0) window.store = store export default store
И сюда же будем передавать наш reducer, который мы создадим отдельно. Создайте папку reducer в src, и соответствующий файл index.js внутри. Как мы уже говорили reducer – это обычная функция, которая принимает текущее состояние и action, и возвращает новое состояние. Так же добавим window, чтобы можно было отслеживать текущее состояний.
Пишем в нашем reducer:
export default (count, action) => { return action.type == "INCREMENT" ? count + 1 : count }
не забудем сделать import в app.js
import store from './store'
Обратимся к документации. а именно к методам store. Нас будут интересовать 3 из них:
Задать состояние мы должны явно при создании. Обратите внимание, что мы уже это сделали, задав состояние равное 0.
Чтобы что то изменить в Store мы должны вызвать action creater с помощью второй метод dispatch. Попробуйте его в console браузера:
store.dispatch({type: ‘INCREMENT’})
Теперь состояние измениться на 1.
Теперь поработаем с отображением. В app.js добавим storeGetState, также мы хотим подписаться на изменения store и передавать их в counter, расстроим еще один способ:
render(<Counter count = {store.getState()} />, document.getElementById('container')) store.subscribe(() => { render(<Counter count = {store.getState()} />, document.getElementById('container')) })
Теперь при каждом обновлении store автоматически обновляется наше число.
Теперь нашу ссылку нужно подружить с action creater.
Создадим файл constants в src в котором будем хранить словарь с доступными типами actions :
export const INCREMENT = 'INCREMENT'
В reducer добавим, и кое что поправим:
import { INCREMENT } from '../constants' export default (count, action) => { return action.type == INCREMENT ? count + 1 : count }
Заведем директорию AC, а в ней файл counter.js:
import { INCREMENT } from '../constants' export function increment() { return { type: INCREMENT } }
В app.js сделаем import, а также dispatch и передадим его в наш counter как props:
function wrappedIncrement() { store.dispatch(increment()) } //render(<ArticleList articles = {articles} />, document.getElementById('container')) render(<Counter count = {store.getState()} increment = {wrappedIncrement}/>, document.getElementById('container')) store.subscribe(() => { render(<Counter count = {store.getState()} increment = {wrappedIncrement} />, document.getElementById('container')) })
counter.js измениться следующим образом:
import React, { Component, PropTypes } from 'react' class Counter extends Component { static propTypes = {
count: PropTypes.number, increment: PropTypes.func
}; render() { return ( <div> <h1>{this.props.count}</h1> <a href="#" onClick = {this.handleIncrement}>increment</a> </div> ) } handleIncrement = (ev) => { ev.preventDefault()
this.props.increment()
} } export default Counter
Вот таким способом, мы закончили простую работу с данными. Мы по клику на ссылку вызываем метод, который пришел к нам через props, этот метод вызывает store.dispatch, при этом оборачивая в него наш простой action creater, который просто создает этот объект, который описывает наш action (в counter.js – type: INCREMENT). Далее наш store, берет и вызывает свой reducer, передавая в него текущее состояние и action. Этот reducer вернет уже новое состояние, store запишет это новое состояние себе и вызовет callback который мы передали в store.subscribe. Этот callback заново вызовет метод render и перестроит заново наш компонент, и мы увидим число на один больше. Так выглядит простой Redux цикл, построенный с нуля.
Код урока находится здесь.
We are looking forward to meeting you on our website soshace.com
здравствуйте автор! большое спасибо вам за труд! но есть некоторые недочеты и опечатки в кодах(различаются коды в материалах сайта и на GITe). а теперь вот в этом разборе домашнего задания ошибка при объявлении ф-ии handleDayClick. с заданными аргументами код не работает, нужно оставить один аргумент day. поправьте пожалуйста…
Спасибо за комментарий! У нас все работает, вот посмотрите:
http://joxi.ru/5md7jWaSkz4xBr
Причина в версии Day-Picker:
http://joxi.ru/E2pdRkNh9NB762
C 5ой версии поменялся порядок объявления аргументов. Мы работали тогда с версией 2.5. Рекомендую поставить старую версию чтобы корректно работали остальные уроки.
Здравствуйте, Автор. Подскажите, пожалуйста, для чего вызов метода dispatch необходимо оборачивать в обертку в виде функции wrappedIncrement?
Если мы передадим store dispatch, то мы его просто вызовем и передадим результат. А в props increment мы должны передать функцию, потому что хотим дать возможность Counter’у делать икремент с помощью this.props.increment()