Trong bài viết này chúng ta sẽ tạo một danh sách chứa các contact sử dụng React và Redux. Chúng ta sẽ có được danh sách contact và mỗi lần click vào một trong những contact này, thì thông tin chi tiết của contact sẽ được hiển thị.
Cái này sẽ giúp chúng ta hiểu về những điểm quan trọng và các Component cơ bản của ứng dụng React + Redux. Bạn có thể dùng project React Simple Starter
Các thành phần chính của React + Redux như sau:
- Reducers
- Containers
- Actions ( và ActionCreators)
Reducer
Reducer là hàm trả về một phần trạng thái của ứng dụng
Giả sử trạng thái với biến là state của ứng dụng như sau:
state = { contacts: [{ "name": "Miguel", "phone": "123456789", },{ "name": "Peter", "phone": "883292300348", },{ "name": "Jessica", "phone": "8743847638473", },{ "name": "Michael", "phone": "0988765553", }], activeContact: { "name": "Miguel", "phone": "123456789", } }
Chúng ta cần có 2 reducer, một cho contact và một cho activeContact.
Ví dụ, danh sách các contact có thể là một phần trạng thái trong ứng dụng của chúng ta và một reducer hợp lệ (không đổi) để nắm giữ danh sách đó sẽ là:
export default function () { return [{ "name": "Miguel", "phone": "123456789", },{ "name": "Peter", "phone": "883292300348", },{ "name": "Jessica", "phone": "8743847638473", },{ "name": "Michael", "phone": "0988765553", }]; }
Chúng ta phải có một “RootReducer” là kết hợp của tất cả Reducer trong ứng dụng (trạng thái của ứng dụng/ Redux store). Để làm việc này, Redux cung cấp một hàm combineReducers
// reducers/index.js import { combineReducers } from 'redux'; import ContactsReducer from './reducer_contacts' import ActiveContactReducer from './reducer_active_contact' const rootReducer = combineReducers({ contacts: ContactsReducer, activeContact: ActiveContactReducer }); export default rootReducer;
Làm thế nào chúng ta kết nối trạng thái đó đến một React Component? Chúng ta cần React Redux package, để giúp chúng kết nối React và Redux. Như thế nào? Đó chính là Container.
Container
Container đóng gói một React Component và cung cấp một phần của trạng thái chúng ta cần để truyền nó xuống như props bên trong React Component.
Những Component nào được coi là Container / Component thông minh và những Component nào là không thông minh? Khi một Component cần biết về một trạng thái cụ thể từ Redux Store, thì nó phải là một Container.
Để kết nối một Component với Redux Store, chúng ta cần import
hàm connect
từ thư viện React-Redux và truyền cho nó những tham số yêu cầu
Giả sử chúng ta có một danh sách các contact trong một Component được gọi là ContactList và chúng ta cần kết nối nó với Redux Store
import React, {Component} from 'react' import {connect} from 'react-redux' import selectContact from '../actions/action_select_contact' import {bindActionCreators} from 'redux' class ContactList extends Component { renderList() { return this.props.contacts.map((contact) => { return ( <li key={contact.phone} onClick={() => this.props.selectContact(contact)} className='list-group-item'>{contact.name}</li> ); }); } render() { return ( <ul className = 'list-group col-sm-4'> {this.renderList()} </ul> ); } } function mapStateToProps(state) { return { contacts: state.contacts }; } export default connect(mapStateToProps[,...later...])(ContactList)
Chú ý rằng hàm mapStateToProps
sẽ thu được toàn bộ trạng thái của Redux Store và chúng ta phải lựa chọn từ đó những cái mà chúng ta cần, sau đó trả về một object được sử dụng như props của ContactList Component. Nên chúng ta xử lý trả về Object như sau
function mapStateToProps(state) { return { amazingContacts: state.contacts } }
Sau khi kết nối đến một ContactList Component, chúng ta có thể lấy được trạng thái của contacts (trong state) thông qua this.props.amazingContacts
Actions
Nếu bạn nghĩ kỹ về các Component, bạn sẽ nhận ra một vài điểm quan trọng: Chúng ta chỉ đọc, nhưng chúng ta không biết cách thay đổi trạng thái của Component. Vì thế, chúng ta cần Action Creator để tạo ra các Action. Những Action này sau đó sẽ được truyền đến tất cả Reducer được kết hợp trong RootReducer và mỗi một trong số chúng sẽ trả về một bản sao mới của trạng thái đã sửa (hoặc không) theo Action được gọi. Reducer của chúng ta sẽ trông giống như thế này (chú ý đến trường hợp mặc định, bởi vì chúng ta luôn phải trả về một trạng thái)
// reducer_active_contact.js export default function(state = null, action) { switch (action.type) { case 'CONTACT_SELECTED': return action.payload } return state }
Chúng ta vẫn còn việc phải làm, chúng ta phải tạo các Action Creator với tên gọi thích hợp cho một hàm mà trả về một object với 2 phần tử: type và payload. Trong số 2 phần tử đấy, chỉ có một phần tử bắt buộc là type (bao gồm tên của type)
// actions/action_select_contact.js function selectContact(contact) { return { type: 'CONTACT_SELECTED', payload: contact } } export default selectContact;
Bây giờ chúng ta có một hàm để tạo các Action, nhưng những Action đó phải chạy qua Redux. Vậy làm thế nào để để tạo các Action chạy qua Redux Reducer? Thông qua hàm connect
với tham số là hàm mapDispatchToProps
:
// ... some other imports... import selectContact from '../actions/action_select_contact' import {bindActionCreators} from 'redux' // The ContactList component goes here // mapStateToProps is here function mapDispatchToProps(dispatch) { return bindActionCreators({selectContact: selectContact}, dispatch); } export default connect(mapStateToProps, mapDispatchToProps)(ContactList)
Hàm này là một trong những đảm bảo rằng bất cứ khi nào Action selectContact
được kích hoạt và bất cứ cái gì được nó trả về sẽ chạy qua tất cả các Reducer. Nếu chúng ta có nhiều Action hơn nữa, thì nó vẫn xử lý tương tự giống như mỗi Action. Chú ý rằng tên của nó có thể khác với từng object
function mapDispatchToProps(dispatch) { return bindActionCreators({ myAction1: action1, myAction2: action2 }, dispatch); }
Nào, bây giờ thì props bên trong ContactList Component sẽ chứa các Action chúng ta đã kết nối (myAction1, myAction2 …)
Bây giờ thì Component của ContactList sẽ như sau:
import React, {Component} from 'react' import {connect} from 'react-redux' import selectContact from '../actions/action_select_contact' import {bindActionCreators} from 'redux' class ContactList extends Component { renderList() { return this.props.contacts.map((contact) => { return ( <li key={contact.phone} onClick={() => this.props.selectContact(contact)} className='list-group-item'>{contact.name}</li> ); }); } render() { return ( <ul className = 'list-group col-sm-4'> {this.renderList()} </ul> ); } } function mapStateToProps(state) { return { contacts: state.contacts }; } function mapDispatchToProps(dispatch) { return bindActionCreators({ selectContact: selectContact }, dispatch); } export default connect(mapStateToProps, mapDispatchToProps)(ContactList)
Bây giờ bước tiếp theo sẽ là:
- Bên trong các Reducer nhận biết Action dựa theo kiểu của Action nhận được.
- Cập nhập trạng thái (Nó sẽ kích hoạt render lại các Component bị ảnh hưởng)
Trong ví dụ của chúng ta, chỉ có Reducer liên quan đến Contact sẽ được lựa chọn như là ActiveContactReducer nhưng chúng ta chưa tạo nó, do đó chúng ta sẽ làm ngay bây giờ đây
Các Reducer sẽ nhận được hai tham số: Phần của trạng thái chúng quan tâm (Không phải toàn bộ trạng thái) và Action hiện tại nào đang chạy qua Reducer. Nhớ rằng, chúng sẽ trả về một bản sao của phần trạng thái bị thay đổi dựa theo Action nhận được.
Trong trường hợp này, phần trạng thái activeContact
sẽ được cập nhập bởi thông tin contact chứa trong action.payload
. Bất cứ giá trị nào hàm trả về sẽ được gán là một phần của trạng thái cho Reducer xử lý. Một Reducer sẽ luôn phải trả về một thứ gì đó:
//reducer_active_contact export default function (state = null, action) { switch (action.type) { case 'CONTACT_SELECTED': return action.payload } return state; }
Bây giờ để hiển thị một contact hay các contact khác, chúng ta cần một <ContactDetail>
Container ( Nhớ rằng nó là một Container bởi vì nó cần thông tin của phần trạng thái trong Redux Store)
Để kết nối Component đến Redux Store, nó giống như trước đây:
Hàm mapStateToProps
được truyền như các tham số trong hàm connect
để kết nối nó với Component và lấy ra thông tin của trạng thái trong biết state như sau
// inside containers/contact-detail.js import { connect } from 'react-redux' // ... ContactDetail component here ... function mapStateToProps(state) { return { contact: state.activeContact //activeContact is defined in the rootReducer } } export default connect(.....)
Reducer được thêm vào rootReducer giống với cách ContactsReducer
import { combineReducers } from 'redux'; import ContactsReducer from './reducer_contacts' import ActiveContactReducer from './reducer_active_contact' const rootReducer = combineReducers({ contacts: ContactsReducer, activeContact: ActiveContactReducer }); export default rootReducer;
Tóm tắt tiến trình
Bây giờ, tất cả mọi thứ đã được kết nối, do đó khi click vào một contact trong danh sách nó nên kích hoạt được Action là selectContact
được truyền đến tất cả Reducer trong rootReducer. ActiveContactReducer
sẽ tương tác lại Action đó bằng cách thay đổi phần trạng thái nó kiểm soát (activeContract), thiết lập nó thành selectedContract
được chứa bên trong action.payload
và khi thay đổi này xảy ra, <ContactDetail>
sẽ nhận biết được trạng thái đã thay đổi, render lại và this.props.contact
đang được chứa giá trị mới.
<ContactDetail>
sẽ được trông như sau:
import React, { Component } from 'react' import { connect } from 'react-redux' class ContactDetail extends Component { render() { if (!this.props.contact) { return ( <div>Select a contact from the list to see its details</div> ); } return ( <div> <h3>Contact Details for: {this.props.contact.name}</h3> <div>Phone: {this.props.contact.phone}</div> </div> ); } } function mapStateToProps(state) { return { contact: state.activeContact } } export default connect(mapStateToProps)(ContactDetail);
Tất cả mọi thứ được kết nối với nhau như thế nào?
Làm thế nào để lúc đầu ứng dụng biết các Reducer nào và ở đâu? (Nhớ rằng trạng thái được xác định bằng cách kết hợp các Reducer bằng combineReducers
)
Nó được thực hiện trong một file chính, nơi mà chúng ta đang xác định ứng dụng bằng cách sử dụng <Provider> Component, thu được các lưu trữ (Store), làm cho nó sẵn sàng cho các Component nó bao trùm.
Để nhận lưu trữ (Nhớ rằng nó là rootReducer) chúng ta sẽ kết hợp nó bằng hàm applyMiddleware, nó sẽ nắm giữ các middleware (Khi chúng ta cần chúng) và truyền trạng thái thông qua chúng trước khi tương tác lại với ứng dụng
import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import { createStore, applyMiddleware } from 'redux'; import App from './components/app'; import reducers from './reducers'; // this exports rootReducer!!! const createStoreWithMiddleware = applyMiddleware()(createStore); ReactDOM.render( <Provider store={createStoreWithMiddleware(reducers)}> <App /> </Provider> , document.querySelector('.container'));