Writer at fiveminutes.today

May 6, 2018, 6:22 p.m.

React và Redux đơn giản cho người mới bắt đầu ! (Phần 2)

Redux với React Native

Bài cùng series:

Phần 1: Nguyên lý cơ bản của Redux

Phần cuối: Redux middleware

Bài trước Nguyên Lý của Redux các bạn đã nắm được cơ bản phần lý thuyết về Redux và dùng cho việc gì, sau đây chúng ta sẽ đi vào áp dụng nó với React hoặc React Native, ở đây mình chọn React Native vì nó vẫn đang còn khá HOT :D (Thực ra React hay React Native thì đều dùng Redux là y hệt nhau)

Đầu tiên các bạn vào link: https://facebook.github.io/react-native/docs/getting-started.html để làm theo và tạo 1 project với React Native.


Lưu ý: Máy phải được cài sẵn yarn (https://yarnpkg.com/en/) hoặc dùng npm sau khi đã cài node, vì nếu cài yarn thì nó sẽ đồng thời cài node luôn cho bạn.

Phần 1: Tạo giao diện màn hình với React Native

Sau khi install xong, tạo thêm thư mục src -> file Main.js như sau:

Ở đây mình sẽ minh họa tạo chức năng hết sức đơn giản là Bấm — Tăng — Giảm 1 số dùng React Native + Redux có màn hình hiển thị lên như sau:

Main.js

import React, {Component} from 'react';
import {View, StyleSheet} from 'react-native';
import Child from './components/Child';
import ButtonComp from './components/Button';
class Main extends Component {
render() {
return (
<View style={{
flex: 1,
width: '100%',
justifyContent: 'center'
}}
>
<View style={{
flex: 1,
justifyContent:"center",
alignItems:"center"
}}>
<Child/>
</View>
<View style = {{flex:1}}>
<ButtonComp
title="Increase"
textColor="#fff"
bgColor="#397af8"
onPress={() =>{}}/>
<ButtonComp
title="Decrease"
bgColor="orange"
onPress={() =>{}}/>
</View>
</View>
)
}
}
export default Main;
const styles = StyleSheet.create({
text: {
fontSize: 24,
fontWeight: "bold",
color: 'red'
},
btnStyle: {
width: 100,
height: 40,
borderWidth: 1,
borderStyle: 'solid',
borderColor: "red",
backgroundColor: "#15c"
},
});

Trong thư mục components ta tạo thêm 1 component có tên Button.js, thực tế bạn không cần phải viết hẳn 1 component chỉ render mỗi cái Button như này, do mình muốn tách nhỏ component nên tạm lấy ví dụ viết 1 component con riêng ra. (trong component này mình có dùng react-native-elements cài đặt nó qua yarn bằng lệnh sau:yarn add react-native-elements

import React, {Component} from 'react';
import {Button} from 'react-native-elements';
const ButtonComp = ({title, onPress, bgColor, textColor}) => (
<Button
style = {{marginBottom:10}}
backgroundColor = {bgColor}
title = {title}
color = {textColor}
onPress = {onPress}/>
);
export default ButtonComp;

Component để hiển thị text number:

import React, {Component} from 'react';
import {View, Text, StyleSheet} from 'react-native';
class Child extends Component {
render() {
return (
<View>
<Text style = {styles.text} >0</Text>
</View>
)
}
}
export default Child;
const styles =StyleSheet.create({
text: {
fontSize: 100,
color:'#000',
}
});

Okay tới đây coi như xong giao diện, như vậy bạn đã có 1 app counter khá đơn giản :D và hoàn toàn có thể làm dùng React Native đơn giản hơn nhiều, nhưng để hiểu về Redux thì chúng ta nên bắt đầu từ việc đơn giản đã.

Phần 2: Cài đặt Redux

Step1: install Redux, React-Redux dependencies

yarn add redux react-redux

Step 2: Tạo cấu trúc thư mục project như hình:

3 nhân tố của Redux là actions, reducers và store tương ứng với 3 thư mục trong project.


actions:

index.js chứa các action (nó sẽ export ra cho các class có thể gọi tới các function bên trong nó)

import {INCREASE, DECREASE} from './type';
export const counterIncrease = () => ({type:INCREASE});
export const counterDecrease = () => ({type:DECREASE});

types.js định nghĩa các hằng số về kiểu của actions:

export const INCREASE = 'increase';
export const DECREASE = 'decrease';

reducers:

Tạo 1 reducer counterReducer: làm nhiệm vụ update state counter mỗi khi có action click

import {INCREASE, DECREASE} from '../actions/type';
const initialState = 0;
export default function (state = initialState, action) {
switch (action.type) {
case INCREASE:
return state + 1;
case DECREASE:
return state - 1;
default:
return state;
}
}

Tiếp theo tạo file index.js, file này có nhiệm vụ combine các reducers con thành 1 reducer duy nhất để đưa vào store

import {combineReducers} from 'redux';
import counterReducer from './counterReducer';
export default combineReducers({
counter:counterReducer
});

Store:

tạo 1 file index.js trong thư mục store. (Mình có config thêm redux-devtools-extension để tiện debug và xem store khi redux hoạt động trên extention của chrome).

import {createStore, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import reducers from '../reducers';
import { composeWithDevTools } from 'redux-devtools-extension';
const store = createStore(
reducers,
{},
composeWithDevTools(
...applyMiddleware(thunk),
)
);
export default store;

Step 3: connect React component với store của Redux

Việc cần làm là làm sao để kết nối toàn bộ components của app với store của Redux.

Thư viện react-redux đã cung cấp 1 thằng có tên Provider để làm cầu nối cho React và Redux, chúng ta chỉ việc bọc nó bao ngoài root component của React và truyền 1 tham số duy nhất là store vào (store đã được tạo ở thư mục store và được import vào file này).

import React from 'react';
import {StyleSheet, View} from 'react-native';
import Main from './src/Main';
import {Provider} from 'react-redux';
import store from './src/store';
export default class App extends React.Component {
render() {
return (
<Provider store = {store}>
<View style={styles.container}>
<Main/>
</View>
</Provider>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});

Step 4: Gọi 1 action từ 1 component và map data từ store ra View

Quay lại bài trước như đã biết thì flow của redux:


action → reducer → store → View

3 bước đã thực hiện xong ở trên, việc còn lại là store và View tương tác như nào, câu hỏi đặt ra là :

Phát đi 1 action từ component → store như thế nào?

Cập nhật data thay đổi từ store → View ra sao?

Nhiệm vụ quan trọng này được thực hiện bởi hàm connect() trong react-redux.

Hàm connect() có 2 tham số:

mapDispatchToProps(dispatch) nhiệm vụ map hàm dispatch() của store trở thành 1 thuộc tính của props của component đó, cụ thể ở code bên dưới thì actions chính là 1 props của component đó và giờ muốn phát đi 1 action ta chỉ việc gọi this.props.actions.tên_action_

const mapDispatchToProps = (dispatch) => ({
actions:bindActionCreators(actionName, dispatch)
});

mapStateToProps(state) nhiệm vụ hết sức đơn giản giống như cái tên của nó, biến các state từ store thành props của component và sau đó show ra View

const mapStateToProps = (state) => ({
state_name:state.reducer_name
});

Để đơn giản và ngắn gọn hơn ta sẽ bỏ đi hàm mapDispatchToProps(dispatch) và thay bằng việc truyền trực tiếp actions vào hàm connect() cuối cùng sẽ là:

connnect(mapStateToProps, actions)(COMPONENT)

Quay trở lại code React Native ở trên, ở phần View ta có:

main.js

import React, {Component} from 'react';
import {View, StyleSheet} from 'react-native';
// import ShowText from './components/ShowText';
import Child from './components/Child';
import ButtonComp from './components/Button';
import * as actions from './actions';
import {connect} from 'react-redux';
class Main extends Component {
handleIncrease = () => {
this.props.counterIncrease();
};
handleDecrease = () => {
this.props.counterDecrease();
};
render() {
return (
<View style={{
flex: 1,
width: '100%',
justifyContent: 'center'
}}
>
<View style={{
flex: 1,
justifyContent:"center",
alignItems:"center"
}}>
<Child/>
</View>
<View style = {{flex:1}}>
<ButtonComp
title="Increase"
textColor="#000"
bgColor="#397af8"
onPress={this.handleIncrease}/>
<ButtonComp
title="Decrease"
bgColor="orange"
onPress={this.handleDecrease}/>
</View>
</View>
)
}
}
export default connect(null, actions)(Main);
const styles = StyleSheet.create({
text: {
fontSize: 24,
fontWeight: "bold",
color: 'red'
},
btnStyle: {
width: 100,
height: 40,
borderWidth: 1,
borderStyle: 'solid',
borderColor: "red",
backgroundColor: "#15c"
},
});

Giải thích:

  • Actions được import từ thư mục actions → được truyền vào là tham số thứ 2 của hàm connect()
  • Mỗi khi button đc bấm thì ta gọi tới hàm handle tương ứng để xử lý actions, nhớ rằng đã thực hiện connnect(null, actions) ở trên thì bây giờ các actions export ra từ file index trong thư mực actions trở thành các thuộc tính của props trong component Main.js → khi gọi tới action thì ta chỉ việc gọi this.props.action_name_tương_ứng_

Tiếp theo là hiển thị ra View ở component child.js

import React, {Component} from 'react';
import {View, Text, StyleSheet} from 'react-native';
import {connect} from 'react-redux';
class Child extends Component {
constructor(props) {
super(props);
console.log("Constructor");
}
render() {
console.log("child", this.props.counter);
return (
<View>
<Text style = {styles.text} >{this.props.counter}</Text>
</View>
)
}
}
const mapStateToProps = state => ({
counter: state.counter
});
export default connect(mapStateToProps, null)(Child);
const styles =StyleSheet.create({
text: {
fontSize: 100,
color:'#000',
}
});

Ở file này ta vừa gán thuộc tính counter : state.counter lúc này counter cũng trở thành 1 props của component Child → gọi show ra View ta chỉ cần gọi như thông thường <Text>{this.props.counter}</Text>

Như vậy là 1 app counter đơn giản bằng React Native và Redux đã thực hiện xong tóm lại flow như sau:

1. View gồm 2 button Increase và Decrease và 1 component hiển thị number

2. Khi Button được click → dispatch() tới 1 action creator có têncounterIncrease()

3. counterIncrease() sẽ tạo ra 1 Object (Trong redux action phải là 1 plain object có thuộc tính là type và payload, type là bắt buộc) ở đây chỉ có 1 thuộc tính {type:"INCREASE"} sau đó nó truyền tới counterReducer() để xử lý.

4. counterReducer(state, action) => kiểm tra xem action có kiểu type = "increase" trả ra 1 state mới là: state + 1 (state của redux là immutable)

5 ở component View(Child) hiển thị number ta sẽ dùng hàm mapStateToProps(state) đẻ nhận state là counter rồi update vào View.

→ App đã chạy ngon lành :D

Giả sử bây giờ yêu cầu bài toán có thay đổi chút như sau:

  • Nếu click vào nút tăng hoặc giảm mà sau 1s con số mới thay đổi.
  • Nâng cao hơn chút là nếu click vào button increase number thì number chạy từ 0 cho tới 1 con số bất kỳ trong khoảng 1s rồi dừng lại.

Bình thường ở bài toán trên ta bấm nút thì ngay lập tức action đáp trả kết quả, nhưng trong thực tế có nhiều bài toán ko lập tức có thay đổi luôn điển hình như call api tới server để fetch data, thì phải mất 1 lúc kết quả mới được trả về. Hay như bài toán bấm nút thì sau 1s mới thay đổi → thì những điều này người ta gọi đó là side-effect .

Như đã biết thì Redux yêu cầu 1 object được trả ra phải là Plain object: {type: ACTION_TYPE, payload:params} và reducer phải là pure function,hay nói cách khác là hoàn toàn ko có side-effect.

Okay có lẽ bài thứ 2 về redux đến đây là dài rồi :))

Mình sẽ giải quyết vấn đề side-effect ở bài sau dùng Redux-middleware

Đọc tiếp phần 3: Redux-middleware (thunk, saga, observable)

Source code ví dụ trên: check out branch lesson_2

Khuyến khích người viết

Bằng cách chia sẻ bài viết của :5 Phút Mỗi Ngày.

0
Writer at fiveminutes.today

May 6, 2018, 6:22 p.m.

0 Comment

Register for News