Уроки React. Урок 7
Всем привет! Сегодня у нас будет довольно важный урок, мы все ближе и ближе подбираемся к Redux, но для начала пробежимся по нашему домашнему заданию, оно было довольно простым, но все таки для проверки покажу как добавить наш календарь.
Установим наш модуль:
npm install react-day-picker —s
Проверим, что в нашем package.json появилась запись об этом:
1 | "react-day-picker": "^2.3.3", |
В ArticleList сделаем import самого Day-picker:
1 2 | import DayPicker, { DateUtils } from "react-day-picker" import 'react-day-picker/lib/style.css' |
State изменим следующим образом:
1 2 3 4 5 | state = { selectedArticles: null, from: null, to: null } |
В наш div добавим к нашему select новый модуль:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <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> |
А ниже опишем логику работы компонента:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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.
1 2 3 4 5 6 | store.dispatch({ type: INCREMENT, data: {amount} }) store.subscribe(callback) |
Так как у нас один store у нас один метод подписки на него. Flux очень гибкий и имеет много store – что скорее всего будет мешать при разработке.
3. Action-creators в Redux это просто чистые функции которые возвращают вам объект, с какими либо данными, которые вместе с этим action.
1 2 3 4 5 6 | function increment (amount) { return { type: INCREMENT, data: {amount} } } |
Диспетчер здесь отсутствует.
4. Stores => Reducers, это механизм перехода между состояниями. Reducers это тоже чистые функции. Это функция которая знает начальное состояние, получив какой то action должна перейти в другое состояние. Они не меняют старое, а возвращают новое:
1 2 3 4 5 | function counter (number = o, action) { const {type, data} = action return type == INCREMENT ? number + data.amount : number } |
Давайте прежде чем делать что-либо сложное сделаем простой Counter с Redux. Будем отображать на странице число и увеличивать его значение при нажатии на кнопку.
Зайдем в app.js и закомментируем наш Articlelist и заведем компонент Counter в папке components:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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 и сделаем импорт компонента:
1 | import Counter from './components/Counter' |
А также добавим:
1 | render(<Counter count = {0} />, document.getElementById('container')) |
Теперь установим Redux в console:
npm i redux –S
Теперь count мы будем хранить в store а не передавать его вручную.
1 | (<Counter count = {0} /> |
Для этого конечно же создадим Store. Создайте директорию с таким названием в src. Там создадим файл index.js. Для начала обратимся к документации Redux.
Прежде чем говорить про store, прочтите главу createStore. Все ваши данные будут жить в нем. Создается он с помощью функции:
createStore(reducer, [preloadedState], [enhancer])
с одним обязательным аргументом – reducer.
Теперь создадим наш store:
1 2 3 4 5 6 7 | 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:
1 2 3 | export default (count, action) => { return action.type == "INCREMENT" ? count + 1 : count } |
не забудем сделать import в app.js
1 | import store from './store' |
Обратимся к документации. а именно к методам store. Нас будут интересовать 3 из них:
Задать состояние мы должны явно при создании. Обратите внимание, что мы уже это сделали, задав состояние равное 0.
Чтобы что то изменить в Store мы должны вызвать action creater с помощью второй метод dispatch. Попробуйте его в console браузера:
store.dispatch({type: ‘INCREMENT’})
Теперь состояние измениться на 1.
Теперь поработаем с отображением. В app.js добавим storeGetState, также мы хотим подписаться на изменения store и передавать их в counter, расстроим еще один способ:
1 2 3 4 5 | render(<Counter count = {store.getState()} />, document.getElementById('container')) store.subscribe(() => { render(<Counter count = {store.getState()} />, document.getElementById('container')) }) |
Теперь при каждом обновлении store автоматически обновляется наше число.
Теперь нашу ссылку нужно подружить с action creater.
Создадим файл constants в src в котором будем хранить словарь с доступными типами actions :
1 | export const INCREMENT = 'INCREMENT' |
В reducer добавим, и кое что поправим:
1 2 3 4 5 | import { INCREMENT } from '../constants' export default (count, action) => { return action.type == INCREMENT ? count + 1 : count } |
Заведем директорию AC, а в ней файл counter.js:
1 2 3 4 5 6 7 | import { INCREMENT } from '../constants' export function increment() { return { type: INCREMENT } } |
В app.js сделаем import, а также dispatch и передадим его в наш counter как props:
1 2 3 4 5 6 7 8 9 10 | 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 измениться следующим образом:
1 2 3 4 | import React, { Component, PropTypes } from 'react' class Counter extends Component { static propTypes = { |
count: PropTypes.number, increment: PropTypes.func
1 2 3 4 5 6 7 8 9 10 11 12 13 | }; render() { return ( <div> <h1>{this.props.count}</h1> <a href="#" onClick = {this.handleIncrement}>increment</a> </div> ) } handleIncrement = (ev) => { ev.preventDefault() |
this.props.increment()
1 2 3 4 | } } 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
0 comments