В сегодняшнем уроке мы займемся уже более сложные вещи. Мы уйдем от ручного описания всех этих “closure”, subscriptions, все это конечно же не делается вручную. Мы научимся делать все значительно проще и элегантнее.
Первое что хотелось бы сделать, это избавиться от всех этих подписок, оборачиваний, как например в app.js. Для этого существует библиотека react-redux, установим ее:
npm i react-redux –S
Сам по себе Redux можно использовать где угодно, а наша библиотека помогает подружить React с Redux и писать меньше кода.
В app.js добавим
import { Provider} from 'react-redux'
и
render( <Provider> <Counter count = {store.getState()} increment = {wrappedIncrement}/>, </Provider>,document.getElementById('container'))
Но обычно это все выносят в отдельный контейнер, создадим папку containers, и создадим в ней файл Root.js. В нем напишем компонент который берет этот Provider и оборачивает в него ваше приложение , собственно Counter.js.
import React, { Component, PropTypes } from 'react' import { Provider } from 'react-redux' import Counter from './Counter' import store from '../store' class RootContainer extends Component { static propTypes = { }; render() { return ( <Provider store = {store}> <Counter /> </Provider> ) } }
Давайте перенесем его в нашу директорию тоже.
В app.js удалим все лишнее оставив следующий код, также добавим Root, и метод render для него:
import React from 'react' import { render } from 'react-dom' import Root from './containers/Root' render(<Root />, document.getElementById('container'))
Этот root container заворачивает все наше предложение и позволяет дальше использовать Redux. В counter мы хотим получить данные, которые хранятся в store (‘count’) и возможность dispatch’ить наши action в этот store (‘increment’). Для этого в react-redux есть connect – это decorator. Добавим его в counter.js:
import { connect } from 'react-redux'
В export default напишем:
export default connect((state) => { return {count: state} }, { increment })(Counter)
Connect принимает в себя 4 параметра, но нужно знать о двух из них. Первый – это функция которая принимает state вашего store и достает из него то, что Вам нужно, сейчас это весь state. Т.е. мы возвращаем объект который merge к нашим props. В итоге мы получим count – как состояние нашего store. Вторым аргументом он принимает объект, в который мы можем передать обычные action creators, и он уже произведет с ними действия такие как: wrapper, dispatch и т.п.
Добавим еще один import:
import { increment } from '../AC/counter'
Проверим, все должно работать. Мы написали много кода и большинство этого кода уже переиспользуется. Connect – довольно умный и умеет намного больше чем просто подписаться на store. Он обновляет обновляет ваш компонент, который меняется, также он добавляет проверку shallow ваших props. Т.е. если у вас не поменялся результат в export, была цифра 1 и осталась 1, то перестраивать компонент он не будет, сделав проверку за Вас.
Следующее что мы сделаем это наконец добавим нормальный store, в котором будет больше данных. Для этого все данные в store мы будем хранить в виде объектов.
Во первых в store (index.js) начальное состояние будет не 0, а объект в котором мы будем хранить все, в том числе наши статьи:
const store = createStore(reducer, {})
Также у нас усложнится reducer. Чтобы не писать большую функцию сделаем несколько reducers в соответствующей папке. Они будут отвечать за count, articles.
articles.js :
export default (articles = [], action) => { return articles }
counter.js :
import { INCREMENT } from '../constants' export default (count = 0, action) => { return action.type == INCREMENT ? count + 1 : count }
Теперь осталось все свести в одну большую функцию, в index.js напишем:
import articles from './articles' import counter from './counter' import { combineReducers } from 'redux' export default combineReducers({ count: counter, articles })
combineReducers – принимает объект, в котором мы описываем ключ – как будет выглядеть элемент в нашем store, будь то articles или count, и какой reducer будет за него отвечать. Ключ – это название поддерева в store, а значение – reducer.
Теперь в containers/Counter.js export будет выглядеть так:
export default connect((state) => { const { count } = state return { count } }, { increment })(Counter)
Проверяем – работает! Вот таким образом мы можем строить достаточно большие store в которых будет храниться много данных, но они будут независимы друг от друга.
В Redux есть разделение на контейнеры и компоненты. «Умные» компоненты, связанные со store такие как Counter. И компоненты, которые просто получают данные и делают их render. У нас сейчас будет два контейнера – Counter и ArticleList.
А теперь добавим в папку containers – Articles.js. Он будет читать статьи из store. Поэтому мы в reducer/articles.js будем брать не просто массив:
import { articles as defaultArticles } from '../fixtures' export default (articles = defaultArticles, action) => { return articles }
В containers/Article.js:
import React, { Component, PropTypes } from 'react' import ArticleList from '../components/ArticleList' import { connect } from 'react-redux' import { deleteArticle } from ‘../AC/articles’ class Articles extends Component { static propTypes = { }; render() { const { articles, deleteArticles } = this.props return <ArticleList articles = {articles} deleteArticle = {deleteArticle} /> } } export default connect( ({articles}) => ({articles}), { deleteArticle } )(Articles)
Также добавим возможность удаления статей. Результаты передаём в connect. В AC создадим articles.js и опишем action creators которые отвечают за статьи:
import { DELETE_ARTICLE } from '../constants' export function deleteArticle(id) { return { type: DELETE_ARTICLE, payload: { id } } }
Также не забудем завести эту константу, в constants.js :
export const DELETE_ARTICLE = 'DELETE_ARTICLE'
Обратимся к нашему reducer – articles.js. Он умеет инициализироваться, но пока никак не обрабатывает никакие actions. Изменим его следующим образом:
import { articles as defaultArticles } from '../fixtures' import { DELETE_ARTICLE } from '../constants' export default (articles = defaultArticles, action) => { const { type, payload } = action switch (type) { case DELETE_ARTICLE: return articles.filter(article => article.id != payload.id) } return articles
Reducers должны возвращать новое состояние не меняя старое. Таким образом мы не можем изменить наш массив. В нашем случае мы можем вернуть отфильтрованный массив с помощью методом filter. Все здесь опирается концепцию immutable данных, которая лежит в основе функционального программирования. Таким образом мы вернем новый список статей, исключая ту которую хотим удалить.
Теперь добавим в containers/Root.js следующий код:|
import Articles from './Articles'
Здесь же в return кое-что изменим. Есть ограничение что в provider мы можем передать только одно ограничение, поэтому их надо завернуть в какой-нибудь <div>. Обычно у вас существует один корневой компонент, но тем не менее, при передаче двух умных компонентов в provider на первый уровень нужно их обернуть:
return ( <Provider store = {store}> <div> <Counter /> <Articles /> </div> </Provider> )
Проверим. Мы научились отображать наши статьи, берем их прямо из store, также у нас есть метод чтобы их удалить, пока мы его не используем. Connect мы будем использовать пока только для того чтобы достать данные. В containers/Article.js изменим код следующим образом:
import React, { Component, PropTypes } from 'react' import ArticleList from '../components/ArticleList' import { connect } from 'react-redux' class Articles extends Component { static propTypes = { }; render() { const { articles } = this.props return <ArticleList articles = {articles} /> } } export default connect( ({articles}) => ({articles}) )(Articles)
Теперь перейдем в Article/index.js добавим:
import { deleteArticle } from '../../AC/articles' import { connect } from 'react-redux'
И завернем нашу статью в connect:
export default connect(null, { deleteArticle })(Article)
Еще добавим кнопку delete:
return ( <div className="article"> <h1 onClick = {openArticle}>{ title } <a href="#" onClick = {this.handleDelete}>delete me</a></h1> <CSSTransitionGroup transitionName="article" transitionEnterTimeout={500} transitionLeaveTimeout = {300}> {body} </CSSTransitionGroup> </div> ) } handleDelete = (ev) => { ev.preventDefault() const { article, deleteArticle } = this.props deleteArticle(article.id) } } export default connect(null, { deleteArticle })(Article)
Теперь проверим – все отлично работает!
Домашнее задание:
Сделать reducer и часть store для фильтров и сделать функционал фильтрации статей. Т.е. чтобы при выборе отрезка времени в календаре отображались только те статьи, которые были добавлены в этих числах. И вынести все эти фильтры из ArticleList в компонент filters. Через Redux пропускаете эти фильтры, таким образом в ArticleList будете отображать отфильтрованные данные.
Код урока можно найти тут.
We are looking forward to meeting you on our website soshace.com
Здравствуйте, Автор. В данном уроке столкнулся с проблемой, диспатча articles. Самое интересное, что в reducer/article.js я вывожу отфильтрованный массив статей, когда приходит экшн DELETE_ARTICLE, все по вашему примеру, но store почему то не перестраивает потом компонент. Вы не могли бы помочь разобраться? привожу ссылку на свой проект на Гитхабе: https://github.com/denis862008/react-training
Добрый день! К сожалению сейчас совсем нет времени посмотреть Ваш код. Посмотрите код следующего урока там все довольна просто. Еще обратите внимание на версии библиотек и.т.п. которые мы использовали тогда для этих статей, лучше придерживаться их для корректной работы.