Уроки React . Урок 8
В сегодняшнем уроке мы займемся уже более сложные вещи. Мы уйдем от ручного описания всех этих “closure”, subscriptions, все это конечно же не делается вручную. Мы научимся делать все значительно проще и элегантнее.
Первое что хотелось бы сделать, это избавиться от всех этих подписок, оборачиваний, как например в app.js. Для этого существует библиотека react-redux, установим ее:
npm i react-redux –S
Сам по себе Redux можно использовать где угодно, а наша библиотека помогает подружить React с Redux и писать меньше кода.
В app.js добавим
1 | import { Provider} from 'react-redux' |
и
1 2 3 4 5 | render( <Provider> <Counter count = {store.getState()} increment = {wrappedIncrement}/>, </Provider>,document.getElementById('container')) |
Но обычно это все выносят в отдельный контейнер, создадим папку containers, и создадим в ней файл Root.js. В нем напишем компонент который берет этот Provider и оборачивает в него ваше приложение , собственно Counter.js.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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 для него:
1 2 3 4 5 | 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:
1 | import { connect } from 'react-redux' |
В export default напишем:
1 2 3 4 5 | export default connect((state) => { return {count: state} }, { increment })(Counter) |
Connect принимает в себя 4 параметра, но нужно знать о двух из них. Первый – это функция которая принимает state вашего store и достает из него то, что Вам нужно, сейчас это весь state. Т.е. мы возвращаем объект который merge к нашим props. В итоге мы получим count – как состояние нашего store. Вторым аргументом он принимает объект, в который мы можем передать обычные action creators, и он уже произведет с ними действия такие как: wrapper, dispatch и т.п.
Добавим еще один import:
1 | import { increment } from '../AC/counter' |
Проверим, все должно работать. Мы написали много кода и большинство этого кода уже переиспользуется. Connect – довольно умный и умеет намного больше чем просто подписаться на store. Он обновляет обновляет ваш компонент, который меняется, также он добавляет проверку shallow ваших props. Т.е. если у вас не поменялся результат в export, была цифра 1 и осталась 1, то перестраивать компонент он не будет, сделав проверку за Вас.
Следующее что мы сделаем это наконец добавим нормальный store, в котором будет больше данных. Для этого все данные в store мы будем хранить в виде объектов.
Во первых в store (index.js) начальное состояние будет не 0, а объект в котором мы будем хранить все, в том числе наши статьи:
1 | const store = createStore(reducer, {}) |
Также у нас усложнится reducer. Чтобы не писать большую функцию сделаем несколько reducers в соответствующей папке. Они будут отвечать за count, articles.
articles.js :
1 2 3 | export default (articles = [], action) => { return articles } |
counter.js :
1 2 3 4 5 | import { INCREMENT } from '../constants' export default (count = 0, action) => { return action.type == INCREMENT ? count + 1 : count } |
Теперь осталось все свести в одну большую функцию, в index.js напишем:
1 2 3 4 5 6 7 8 | 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 будет выглядеть так:
1 2 3 4 5 6 | 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 будем брать не просто массив:
1 2 3 4 5 6 | import { articles as defaultArticles } from '../fixtures' export default (articles = defaultArticles, action) => { return articles } |
В containers/Article.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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 которые отвечают за статьи:
1 2 3 4 5 6 7 8 | import { DELETE_ARTICLE } from '../constants' export function deleteArticle(id) { return { type: DELETE_ARTICLE, payload: { id } } } |
Также не забудем завести эту константу, в constants.js :
1 | export const DELETE_ARTICLE = 'DELETE_ARTICLE' |
Обратимся к нашему reducer – articles.js. Он умеет инициализироваться, но пока никак не обрабатывает никакие actions. Изменим его следующим образом:
1 2 3 4 5 6 7 8 9 10 11 | 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 следующий код:|
1 | import Articles from './Articles' |
Здесь же в return кое-что изменим. Есть ограничение что в provider мы можем передать только одно ограничение, поэтому их надо завернуть в какой-нибудь <div>. Обычно у вас существует один корневой компонент, но тем не менее, при передаче двух умных компонентов в provider на первый уровень нужно их обернуть:
1 2 3 4 5 6 7 8 | return ( <Provider store = {store}> <div> <Counter /> <Articles /> </div> </Provider> ) |
Проверим. Мы научились отображать наши статьи, берем их прямо из store, также у нас есть метод чтобы их удалить, пока мы его не используем. Connect мы будем использовать пока только для того чтобы достать данные. В containers/Article.js изменим код следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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 добавим:
1 2 3 | import { deleteArticle } from '../../AC/articles' import { connect } from 'react-redux' |
И завернем нашу статью в connect:
1 | export default connect(null, { deleteArticle })(Article) |
Еще добавим кнопку delete:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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
0 comments