Flux คืออะไร ? + สอนวิธีนำไปใช้กับ React

Suranart Niamcome
SiamHTML
Published in
10 min readJun 20, 2015

วันนี้ SiamHTML จะขอพูดถึงแนวทางการเขียน JavaScript แบบใหม่ที่มีชื่อเรียกว่า Flux ครับ แต่อย่าเพิ่งตกใจนะครับว่า Flux นั้นเป็น JavaScript framework ตัวใหม่ที่เราจะต้องมาเรียนรู้กันอีก เพราะจริงๆ แล้วมันก็เป็นแค่ “pattern” หรือ “architecture” ของการเขียนโค้ดเท่านั้นเอง เรียกว่าคล้ายๆ กับการที่เราเขียนโค้ดโดยยึดหลัก MVC นั่นแหละครับ เรามาดูกันว่า Flux นั้นจะช่วยให้การเขียนโค้ดของเราดีขึ้นได้มากแค่ไหน ?

ปกติเราเขียน React กันอย่างไร ?

จากบทความก่อนหน้านี้ ที่ SiamHTML ได้เล่าถึงการทำเว็บแบบ Isomorphic ด้วย React นั้น หากสังเกตดีๆ จะเห็นว่ามันเป็นแค่แอปแบบง่ายๆ เท่านั้นเอง ถูกมั้ยครับ ? เรียกว่าแทบจะไม่มี logic อะไรเลย ทีนี้ถ้าเราจะทำแอปกันจริงๆ มี logic ซับซ้อนยิ่งขึ้น มีการดึงข้อมูลมาจากฐานข้อมูล ถามว่าโค้ด logic พวกนี้ เราควรจะเขียนไว้ที่ไหน ? ภายใน component งั้นหรอ ? ไม่ดีแน่ๆ ครับ

รู้จักกับ Flux

ลองนึกเล่นๆ นะครับว่า ถ้าเราเอา application logic ไปกองไว้ตาม component ต่างๆ เราจะลำบากแค่ไหน debug ก็ยาก เขียน test ก็ยาก แถมยัง reuse ไม่ได้อีก เพื่อเป็นการแก้ปัญหาเหล่านี้ วิศวกรของ facebook เค้าจึงคิดค้น Flux ขึ้นมาครับ

อย่างที่บอกนะครับว่า Flux นั้นเป็นแค่ pattern ของการเขียนโค้ดเท่านั้นเอง ซึ่งหมายความว่าตัวมันเองไม่ได้มีไฟล์อะไรเลย มันเป็นแค่ “แนวคิด” ที่ทีม facebook เค้าใช้ในการเขียน React เพื่อช่วยให้การทำงานเป็นไปอย่างราบรื่นเท่านั้นเอง

ผมขอทวนก่อนนะครับว่า React นั้นมี data flow แบบ Unidirectional ซึ่งก็คือระบบที่ข้อมูลจะไหลไปในทิศทางเดียว Flux นั้นก็เหมือนกันครับ เพียงแต่มันจะมีการกำหนดการ flow ของข้อมูลให้มีระบบมากขึ้น เราลองมาดูกันคร่าวๆ นะครับว่า data flow ของ Flux นั้นมีหน้าตาเป็นอย่างไร

flux simple diagram

เห็นมั้ยครับว่าการ flow ของข้อมูลนั้นจะเป็นแบบทางเดียว คือจะเริ่มจากการมี Action ขึ้นมาก่อน จากนั้นก็จะส่งข้อมูลต่อไปยัง Dispatcher และ Store ตามลำดับ และสุดท้ายข้อมูลก็จะถูกส่งไปถึง View ครับ

แต่ในความเป็นจริงแล้ว เว็บแอปส่วนใหญ่มักจะมี interaction กับ user ถูกมั้ยครับ ดังนั้น ตัว View เอง ก็สามารถทำให้เกิด Action ขึ้นมาได้เช่นกันครับ data flow จริงๆ จึงจะเปลี่ยนเป็นแบบนี้

flux diagram

ก่อนจะไปต่อ ผมขอแนะนำให้จำ diagram นี้ให้ขึ้นใจเลยนะครับ แต่ตอนนี้เราอาจจะยังงงๆ อยู่ว่า แล้วแต่ละตัวมันคืออะไร ? เดี๋ยวผมจะไล่อธิบายให้ฟังทีละตัวเลยครับ

เจาะลึก Data Flow ของ Flux

เอาล่ะครับ เรามาดูกันว่า data flow ของ Flux นั้นประกอบไปด้วยอะไรบ้าง

Action

Action ก็คือสิ่งที่เกิดขึ้นเมื่อมี user มาทำอะไรบางอย่างกับ View ครับ (หรือเราจะสั่งให้เกิด Action ขึ้นมาเองเลยก็ได้) สมมติหน้าเว็บเรามีกล่องค้นหาข้อมูลอยู่อันหนึ่ง เมื่อใดก็ตามที่ user กดปุ่ม submit ฟอร์มค้นหา เมื่อนั้นจะมี Action เกิดขึ้นมาครับ แน่นอนว่าข้อมูลที่เราอยากจะผูกไปพร้อมกับ Action นี้ก็คือ keyword ที่ user เค้ากรอกมา ถูกมั้ยครับ คำถามคือ แล้วเราจะส่ง keyword ที่ว่านี้ ไปยังโค้ดส่วนที่จะทำหน้าที่ query ข้อมูลจากฐานข้อมูลได้อย่างไร ?

Dispatcher

วิธีที่ Flux ใช้ก็คือการนำสิ่งที่เรียกว่า Dispatcher มาเป็นตัวกลางครับ โดย Action ดังกล่าวจะต้องไปบอก Dispatcher ว่า 1) ตัวเองคือ Action ประเภทไหน และ 2) มีข้อมูลอะไรที่จะพ่วงมากับ Action นี้บ้าง จากนั้น Dispatcher จะรับหน้าที่ดูว่า Action ดังกล่าว ควรจะวิ่งไปทางไหนต่อครับ

Store

Store คือ ที่ๆ เราจะเก็บข้อมูลต่างๆ ของแอป รวมไปถึง state เอาไว้ครับ นอกจากข้อมูลต่างๆ แล้ว ภายใน Store ก็จะมีการเก็บ method ต่างๆ ที่จะใช้ในการจัดการกับข้อมูลภายใน Store เอาไว้ด้วย หรือพูดง่ายๆ ก็คือมันเป็นที่ๆ เราจะเขียน application logic ต่างๆ นั่นเองครับ อย่างในกรณีนี้ Store จะต้องรับหน้าที่นำ keyword ที่ user กรอกมา ไป query ข้อมูลในฐานข้อมูล แล้วนำผลลัพธ์ที่ได้ ส่งต่อไปยัง View เพื่อแสดงผล แต่แอปเราจะรู้ได้อย่างไรว่า method ไหนใน Store ที่จะต้องจับคู่กับ Action ที่เข้ามาหา ?

คำตอบคือเราจะต้องนำ method ต่างๆ ใน Store ไป register ไว้กับ Dispatcher ก่อนครับ พูดง่ายๆ ก็คือ เราจะต้องจับคู่นั่นเองว่า ถ้ามี Action แบบนี้เข้ามา จะให้วิ่งไปใช้ method ตัวไหน สมมติว่ามี Action ที่ต้องการจะ query ข้อมูลจากฐานข้อมูลเข้ามา เราก็จะต้องจับคู่ Action นี้ เข้ากับ method ที่จะทำหน้าที่เชื่อมต่อกับฐานข้อมูลอะไรทำนองนี้ครับ เมื่อได้ข้อมูลที่ต้องการมาแล้ว Store ก็จะอัพเดทข้อมูลต่างๆ รวมไปถึง state ที่ตัวเองดูแลอยู่ แล้วก็แจ้งกลับไปยัง View เพื่อร้องขอให้มีการ render ใหม่

View

เมื่อ View ได้รับคำร้องขอจาก Store แล้ว View ก็จะไปดึงข้อมูลที่จะต้องใช้ในการ render ใหม่ มาจาก Store จากนั้น View ก็จะ render ตัวเอง และ component ลูกๆ ที่อยู่ภายใน View นั้นๆ ครับ

Workshop — ทำแอป Discussion

มาถึงตรงนี้ หากใครยังงงๆ ก็ขอบอกเลยว่าเป็นเรื่องปกติครับ แนะนำให้อ่านด้านบนเพื่อทบทวนอีกรอบ เพราะตัวผมเองกว่าจะเข้าใจ Flux อย่างละเอียดก็ล่อไปเป็นเดือนเลยเหมือนกัน ของอย่างนี้มันต้องลงมือทำเลยถึงจะเข้าใจครับ ผมว่าเรามาลองเขียนแอปด้วย Flux กันเลยดีกว่า

สมมติว่าโจทย์ของเราคือการสร้างระบบ Discussion แบบง่ายๆ คือเราจะมีฟอร์มอันนึง ให้คนเข้ามา comment อะไรก็ได้ ลองดูซิว่า requirement แบบนี้ เราจะต้องเขียนโค้ดอย่างไร ?

1. เตรียมไฟล์และจัดโครงสร้าง

เริ่มด้วยการสร้างไฟล์และโฟลเดอร์ตามด้านล่างนี้ก่อนเลยครับ

myApp/
|
|-- js/
| |
| |-- actions/ # เก็บ Action ต่างๆ
| |-- components/ # เก็บ component ต่างๆ (.jsx)
| |-- constants/ # เก็บชื่อประเภทของ Action ต่างๆ
| |-- dispatcher/ # เก็บ Dispatcher
| |-- stores/ # เก็บ Store ต่างๆ
| |
| |-- app.js # ไฟล์สำหรับ render component ของ React
| `-- bundle.js # ไฟล์รวม script ที่ได้จากการรัน browserify
|
|-- gulpfile.js # ไฟล์สำหรับทำ automated task ด้วย Gulp
|
`-- index.html # ไฟล์ html ของหน้าเว็บ

ก็ลองดูคร่าวๆ กันก่อนนะครับว่าแต่ละไฟล์แต่ละโฟลเดอร์นั้นมีหน้าที่เก็บอะไร จากนั้นก็ให้เราไล่ใส่โค้ดตามด้านล่างนี้ต่อได้เลยครับ

index.html

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Flux Tutorial by SiamHTML</title>
</head>
<body>
<div id="app"></div>
<script src="js/bundle.js"></script>
</body>
</html>

สังเกตนะครับว่าใน body ของเราจะต้องมี #app อยู่ด้วย เพราะเราจะต้องหาที่เอาไว้ render แอปของเราครับ ส่วนก่อนปิด body เราก็จะต้องใส่ script เข้ามาตัวนึง ซึ่งมันก็คือไฟล์ที่ได้จากการรวม script ทั้งหมดของแอปเรานั่นเองครับ

gulpfile.js

'use strict';

var gulp = require('gulp');
var streamify = require('gulp-streamify')
var uglify = require('gulp-uglify');
var browserify = require('browserify');
var reactify = require('reactify');
var source = require('vinyl-source-stream');

gulp.task('js', function() {
browserify('./js/app.js') // ให้ app.js เป็น entry point
.transform(reactify)
.bundle()
.pipe(source('bundle.js')) // ตั้งชื่อไฟล์ output เป็น bundle.js
.pipe(streamify(uglify()))
.pipe(gulp.dest('./js/'));
});

gulp.task('default', ['js'], function() {
gulp.watch(['./js/**/*', '!./js/bundle.js'], ['js']);
});

ในบทความนี้ ผมจะขอใช้ browserify ในการทำ bundle นะครับ ให้เราเลือก entry point เป็น app.js ได้เลย แล้วตั้งชื่อไฟล์ output ให้เป็น bundle.js ครับ

2. สร้าง Component ต่างๆ แบบ Static

ทีนี้เราจะมาเริ่มสร้าง component กันเลยครับ โดยผมจะขอเริ่มจาก component ที่อยู่นอกสุดก่อนละกัน

DiscussionApp.jsx

ให้เราสร้างไฟล์ใหม่ขึ้นมาอันนึง แล้วตั้งชื่อว่า DiscussionApp.jsx ครับ จากนั้นก็ใส่โค้ดเริ่มต้นตามด้านล่างนี้ลงไป

var React = require('react');// โหลด component ลูก มาใช้
var DiscussionForm = require('./DiscussionForm.jsx');
var DiscussionList = require('./DiscussionList.jsx');
// สร้าง component แม่
var DiscussionApp = React.createClass({

render: function() {
return (
<div>
<DiscussionForm />
<DiscussionList />
</div>
);
}
});
module.exports = DiscussionApp;
ผมมองว่าแอป Discussion นี้ ประกอบไปด้วย 2 ส่วนหลักๆ ก็คือ 1) ส่วนของฟอร์มที่จะเอาไว้พิมพ์ comment และ 2) ส่วนที่จะเอาไว้แสดง comment ผมเลยจะขอแยก 2 ส่วนนี้ ออกมาเป็น component ลูก ของ DiscussionApp ครับ
DiscussionForm.jsxทีนี้มาดูที่ component ลูก อย่าง DiscussionForm กันบ้างครับ ให้เราใส่โค้ดสำหรับ render ฟอร์มธรรมดาๆ ตามด้านล่างนี้ลงไปvar React = require('react');var DiscussionForm = React.createClass({

// render ออกมาเป็น form แบบง่ายๆ
render: function() {
return (
<form>
<input type="text"
placeholder="Enter message here..."
/>
<button>Comment</button>
</form>
);
}
});
module.exports = DiscussionForm;DiscussionList.jsxมาต่อกันที่ component ลูกอีกตัวอย่าง DiscussionList ที่เราจะเอาไว้แสดง comment ครับ ให้เราใส่โค้ดตามนี้ไปได้เลยvar React = require('react');// สร้าง component ที่จะใช้แสดงตัว comment
var DiscussionComment = React.createClass({
render: function() {

// รับข้อมูล comment ที่จะแสดงผ่านทาง props
var comment = this.props.comment;
return (
<li>{comment.title}</li>
);
}
});
// สร้าง component ที่จะเอาไว้ทำ iteration
var DiscussionList = React.createClass({
render: function() {

// วนลูป array ของ comments ที่ได้มาจาก props
// แล้วส่งต่อให้ DiscussionComment นำไปแสดงผล
var DiscussionComments = this.props.comments.map(function(data, index) {
return (
<DiscussionComment key={data.ID} comment={data} />
);
});
return (
<ul>
{DiscussionComments}
</ul>
);
}
});
module.exports = DiscussionList;
จากโค้ดจะเห็นว่า DiscussionList จะรับ props ที่ชื่อ comments มาวนลูปเพื่อแสดงข้อมูล comment โดยใช้ component ลูกอีกตัว ที่ชื่อ DiscussionComment ครับ คือขอแค่ส่ง props ที่ชื่อ comments มาเหอะ แล้ว DiscussionList จะแสดงข้อมูลออกมาให้เอง
app.jsสุดท้ายเรามาดูที่ไฟล์ entry point ของเรา อย่าง app.js กันบ้างครับ ให้เราใส่โค้ดสำหรับ render DiscussionApp เข้าไปใน #app แบบนี้var React = require('react');
var ReactDOM = require('react-dom');
// โหลด component หลักมา
var DiscussionApp = require('./components/DiscussionApp.jsx');
// แสดงผล component หลัก ที่ #app
ReactDOM.render(<DiscussionApp /> , document.getElementById('app'));
มาถึงตรงนี้ โครงของแอปเราเริ่มจะโอเคแล้วล่ะครับ ที่เหลือก็แค่ทำฟอร์มให้สามารถรับข้อมูล comment ได้จริง แล้วก็นำข้อมูลมา assign ให้ DiscussionList เท่านั้นเอง
3. ใส่ Interaction ด้วย Stateทีนี้เราจะมาทำแอปของเราให้ใช้งานได้จริงครับ จาก requirement เราจะเห็นว่าข้อมูลที่จะ flow ไปตามส่วนต่างๆ ของแอปนี้ก็คือ comment ถูกมั้ยครับ ? ซึ่ง comment เหล่านี้จะถูกสร้างขึ้นมาจาก DiscussionForm แล้วจึงถูกนำไปแสดงที่ DiscussionList อีกที หากเป็นแบบนี้ เราจะต้องเก็บ comment เอาไว้ใน state ของ component ตัวแม่ ซึ่งก็คือ DiscussionApp ครับ เพราะมันจะทำให้ DiscussionList สามารถนำ comment ที่สร้างโดย DiscussionForm ไปใช้ได้ด้วย เพื่อให้เห็นภาพมากขึ้นเรามาเริ่มเขียนโค้ดกันเลยดีกว่าครับ

DiscussionApp.jsx

เริ่มกันที่ component ตัวแม่ก่อนเลยครับ ให้เราเพิ่ม method getInitialState() และ _addComment() เข้าไปแบบนี้var DiscussionApp = React.createClass({

// กำหนด allMessages ให้เป็น state ที่มีค่าเริ่มต้นเป็น array เปล่าๆ
getInitialState: function() {
return {
allMessages: []
};
},
// สร้าง method สำหรับเซฟ comment
_addComment: function(message) {

// สร้าง unique id ขึ้นมา
var id = (+new Date() + Math.floor(Math.random() * 999999)).toString(36);

// เตรียม object ของ comment ที่จะเซฟ
var newMessage = [{
'ID': id,
'title': message
}];
// รวม comment ใหม่ เข้ากับ comment ที่มีอยู่เดิม
this.setState({
allMessages: newMessage.concat(this.state.allMessages)
});
},

// ส่ง method สำหรับเซฟ comment ไปให้ DiscussionForm ผ่าน props
// assign ข้อมูล comment ทั้งหมด ให้ DiscussionList นำไปแสดงผล
render: function() {
return (
<div>
<DiscussionForm handleSubmit={this._addComment} />
<DiscussionList comments={this.state.allMessages} />
</div>
);
}
});
ไฮไลท์นั้นจะอยู่ที่การส่ง method _addComment() ไปให้ DiscussionForm ใช้ ผ่าน props ที่ชื่อ handleSubmit ครับ ที่เราต้องใช้ท่านี้ในการเซฟ comment ก็เพราะว่าหากเราให้ DiscussionForm เป็นคนเซฟ comment จากการ submit ลง state ซะเอง เราจะไม่สามารถนำ state นั้น ออกมาใช้ข้างนอก DiscussionForm เพื่อส่งต่อไปยัง DiscussionList ได้นั่นเอง
DiscussionForm.jsxเอาล่ะครับ เมื่อได้รับ method _addComment() มาจาก component แม่แล้ว มาดูว่าเราจะเอาไปใช้ใน DiscussionForm อย่างไร ?var DiscussionForm = React.createClass({

// กำหนด message ให้เป็น state ที่มีค่าเริ่มต้นเป็นค่าว่าง
getInitialState: function() {
return {
message: ''
};
},
// เมื่อ user พิมพ์ comment ให้อัพเดท message
// ให้เป็นค่าเดียวกับที่ user พิมพ์มา
_onChange: function(event) {
this.setState({
message: event.target.value
});
},
// เมื่อฟอร์มถูก submit ให้เซฟค่า message ที่อยู่ใน state
// โดยใช้ method ที่ได้รับมาจาก props ที่ชื่อ handleSubmit
_onSubmit: function(event) {
event.preventDefault();
this.props.handleSubmit(this.state.message);
// จากนั้นก็ reset ค่า message ให้เป็นค่าว่างเหมือนเดิม
this.setState({
message: ''
});
},

// ผูก event ต่างๆ เข้ากับ element
// พร้อมกับกำหนด value ของช่องกรอก comment ให้มีค่าตาม message
render: function() {
return (
<form onSubmit={this._onSubmit}>
<input type="text"
placeholder="Enter message here..."
onChange={this._onChange}
value={this.state.message}
/>
<button onClick={this._onSubmit}>Comment</button>
</form>
);
}
});
เมื่อไล่โค้ดดู ก็จะเห็นว่าเราเรียกใช้ method _addComment() ผ่าน props ที่ชื่อ handleSubmit ครับ เพียงเท่านี้ เราก็จะสามารถเซฟ message ที่เกิดขึ้นภายใน DiscussionForm ไปเก็บไว้เป็น state ของ DiscussionApp ได้แล้วล่ะครับ
มาถึงตอนนี้ก็ลองรันดูเลยครับ ให้เราลองพิมพ์ comment อะไรก็ได้ แล้วลอง submit ดู เราก็จะเห็นว่าแอปของเรานั้นทำงานได้แล้ว แต่ช้าก่อน... นี่ยังไม่ใช่วิธีที่ถูกต้อง !ถึงแม้ว่าแอปของเราจะใช้งานได้แล้วก็จริง แต่การกอง application logic รวมกันไว้ใน component นั้นไม่ดีแน่นอนครับ ไม่ว่าจะเป็นเรื่องของการ debug โค้ด การ reuse โค้ด รวมไปถึงการ test โค้ด แล้วที่สำคัญก็คือ สิ่งที่เก็บไว้ใน state นั้น ควรจะเป็นแค่ข้อมูลเล็กๆ ที่จะเอาไว้บรรยายถึง "สภาพ" ของ component ในขณะนั้นว่าเป็นอย่างไร ไม่ใช่เอา state ไปเก็บข้อมูลที่จะนำมาแสดงผลแบบที่เราทำอยู่ในตอนนี้4. เปลี่ยนมาใช้ Fluxเพื่อเป็นการแก้ปัญหาดังกล่าว เราจะลองเปลี่ยนมาเขียนตามแนวคิดของ Flux ดูบ้างครับ

4.1 สร้าง Dispatcher ขึ้นมาก่อน

สำหรับ data flow แบบ Flux แล้ว Dispatcher นั้นถือเป็นศูนย์กลางของข้อมูลเลยนะครับ เพราะมันจะต้องคอยดูว่าถ้ามี Action เข้ามาแบบนี้ จะต้องส่งต่อไปยัง method ไหนใน Store สำหรับโค้ดในส่วนของ Dispatcher นั้น เราไม่ต้องไปเขียนเองนะครับ เพราะ Flux เค้าได้เตรียมมาให้เรียบร้อยแล้ว สิ่งที่เราต้องทำก็แค่ติดตั้ง Flux ผ่าน npm แบบนี้ครับnpm install flux --saveจากนั้นก็ให้เราสร้างไฟล์ชื่อ AppDispatcher.js ขึ้นมา แล้วใส่โค้ดเรียกใช้ Dispatcher จาก Flux ลงไป// โหลด Dispatcher จาก Flux มาใช้งาน
var Dispatcher = require('flux').Dispatcher;
module.exports = new Dispatcher();
เพียงเท่านี้ เราก็จะได้ความสามารถของ Dispatcher จาก Flux มาใช้แล้วล่ะครับ ให้เราเอาไฟล์นี้ไปวางไว้ในโฟลเดอร์ dispatcher ได้เลย
4.2 ดูว่ามี Action ประเภทไหนบ้าง ?จากนั้นให้เราลิสต์มาให้หมดครับว่า แอปของเรามีประเภทของ Action หรือ actionType อะไรบ้าง จากตัวอย่างก่อนหน้านี้ เราจะมีเพียงแค่ Action เดียว ซึ่งก็คือ Action ที่เอาไว้เซฟ comment ถูกมั้ยครับ ให้เราสร้างไฟล์ DiscussionConstants.js ขึ้นมา แล้วใส่โค้ดด้านล่างนี้ลงไปvar keyMirror = require('keymirror');module.exports = keyMirror({
DISCUSSION_CREATE: null
// หากอนาคตจะเพิ่มฟีเจอร์ให้สามารถแก้ไขและลบ comment ได้ด้วย
// ก็ให้เพิ่ม actionType ตรงนี้ได้เลย
// DISCUSSION_EDIT: null,
// DISCUSSION_DELETE: null
});
โค้ดด้านบนจะเป็นการบอกว่าแอปนี้ มี actionType ที่ชื่อ DISCUSSION_CREATE เพียงแค่อันเดียวครับ หากเราทำแอปที่ซับซ้อนกว่านี้ ก็ให้เราเพิ่ม actionType ต่อท้ายเข้าไปได้เลย ส่วนสาเหตุที่เราต้องเก็บประเภทของ Action เอาไว้เป็น constant แบบนี้ ก็เพราะว่าเราจะใช้มันในการจับคู่ Action เข้ากับ method ของ Store นั่นเองครับ
4.3 สร้าง Action ขึ้นมาจากนั้นเราจะมาสร้างตัว Action จริงๆ กันครับ โดยเราจะอาศัย Dispatcher ในการส่ง parameter ที่จำเป็นเข้าไปให้ Store และอาศัย actionType เพื่อเอาไว้บอก Dispatcher ว่า Action นี้ ควรจะจับคู่กับ Store ตัวไหน// โหลด Dispatcher มาใช้
var AppDispatcher = require('../dispatcher/AppDispatcher');
// โหลด actionType ต่างๆ มาใช้
var DiscussionConstants = require('../constants/DiscussionConstants');
// รวม Action ต่างๆ ในแอป
var DiscussionActions = {
// เมื่อเกิด Action นี้ให้ทำอะไร ?
addComment: function(comment) {

// บอกว่า Action นี้ เป็นแบบ DISCUSSION_CREATE นะ
// พร้อมกับส่งค่า comment พ่วงไปด้วย
AppDispatcher.dispatch({
actionType: DiscussionConstants.DISCUSSION_CREATE,
comment: comment,
});
}
};module.exports = DiscussionActions;4.4 ย้าย Logic มาไว้ใน Store + ผูก Method ของ Store เข้ากับ Actionทีนี้มาดูที่ Store กันบ้างครับ ให้เราย้าย application logic มาเขียนเป็น method ต่างๆ ของ Store แทน จากนั้นก็ใส่โค้ดสำหรับจับคู่ method เหล่านั้นเข้ากับ actionType ประเภทต่างๆ ครับvar AppDispatcher = require('../dispatcher/AppDispatcher');
var DiscussionConstants = require('../constants/DiscussionConstants');
// setup Store (copy ได้เลย เพราะมันเป็น pattern)
var EventEmitter = require('events').EventEmitter;
var assign = require('object-assign');
var CHANGE_EVENT = 'change';
// กำหนดค่าเริ่มต้นให้กับ state และข้อมูล comment
var _state = {};
var _comments = [];
// private method ของ Store สำหรับเซฟ comment
// (ย้ายมาจาก DiscussionApp)
function addData(comment) {
var id = (+new Date() + Math.floor(Math.random() * 999999)).toString(36);
var newMessage = [{
'ID': id,
'title': comment
}];
_comments = newMessage.concat(_comments);
}
// public method ต่างๆ ของ Store
var DiscussionStore = assign({}, EventEmitter.prototype, {
// ดึงข้อมูล state ในตอนนั้นๆ
getState: function() {
return _state;
},
// ดึง comment ทั้งหมดในตอนนั้นๆ
getAll: function() {
return _comments;
},
// 3 methods ด้านล่างนี้เป็น pattern
// ใช้สำหรับแจ้ง component หลัก ว่าถึงเวลา render ใหม่แล้ว
// เนื่องจากข้อมูลมีการอย่างเปลี่ยนแปลง
emitChange: function() {
this.emit(CHANGE_EVENT);
},
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
}
});// จับคู่ method ของ Store เข้ากับ actionType ต่างๆ
AppDispatcher.register(function(action) {
// เมื่อ Dispatcher ได้รับ actionType แบบนี้ ให้ใช้ method ตัวไหน
switch (action.actionType) {
case DiscussionConstants.DISCUSSION_CREATE:
addData(action.comment);
break;
default:
// no op
}

// พอทำ callback เสร็จแล้ว ก็แจ้งกลับไปยัง component หลัก
// ว่าเกิดการเปลี่ยนแปลงแล้วนะ
DiscussionStore.emitChange();
});
module.exports = DiscussionStore;4.5 ลบ Logic เดิมออก แล้วเปลี่ยนมาใช้ Action แทนจากนั้นกลับมาดูที่ DiscussionForm ในส่วนของ method _onSubmit() นิดนึงครับ ให้เราเปลี่ยนจากการเซฟ comment ด้วย this.props.handleSubmit() มาเป็นการใช้ Action ที่เราสร้างขึ้นมาแทน// โหลด Action ที่สร้างไว้มาใช้
var DiscussionActions = require('../actions/DiscussionActions');
var DiscussionForm = React.createClass({

...
_onSubmit: function(event) {
event.preventDefault();

// เปลี่ยนมาใช้ Action แทนการใช้ method จาก component แม่
DiscussionActions.addComment(this.state.message);

this.setState({
message: ''
});
},

...
});4.6 เชื่อม Component หลัก เข้ากับ Storeสุดท้ายแล้ว เราจะต้องเปิดการเชื่อมต่อระหว่าง DiscussionApp กับ Store ครับ ให้เราแก้ไฟล์ DiscussionApp.js ตามโค้ดด้านล่างนี้ได้เลย// โหลด Store มาใช้
var DiscussionStore = require('../stores/DiscussionStore');
// โหลด Action มาใช้
var DiscussionActions = require('../actions/DiscussionActions');
var DiscussionApp = React.createClass({

// 4 methods ด้านล่างนี้เป็น pattern
getInitialState: function() {
// ดึง state ผ่านทาง Store แทน
return DiscussionStore.getState();
},
componentDidMount: function() {
// รอฟังว่า Store จะแจ้งว่ามีการเปลี่ยนแปลงเมื่อไร
DiscussionStore.addChangeListener(this._onChange);
},
componentWillUnmount: function() {
// พอ component จะถูกถอดออก ก็เลิกรอ
DiscussionStore.removeChangeListener(this._onChange);
},
_onChange: function() {
// เมื่อ Store แจ้งว่ามีการเปลี่ยนแปลง ก็สั่งให้อัพเดท state ซะ
// โค้ดตรงนี้แหละ ที่ทำให้ component ถูก render ใหม่
this.setState(DiscussionStore.getState());
},
render: function() {

// ดึง comment จาก Store แทนการดึงจาก state
var comments = DiscussionStore.getAll();
return (
<div>
<DiscussionForm />
<DiscussionList comments={comments} />
</div>
);
}
});
จากนั้นก็ลองรันดูอีกทีเลยครับ จะเห็นว่าแอปเรายังคงสามารถทำงานได้ตามเดิม เพียงแต่โค้ดมันจะเป็นระเบียบมากขึ้นเยอะครับ คือ component ก็จะมีแต่ template อย่างเดียว ไม่มี application logic มาแทรกอยู่ด้วยแล้ว
5. ดึงข้อมูลจาก APIถึงแม้ว่าแอปเราจะทำงานได้แล้วก็จริง แต่มันก็ยังไม่ค่อยจะสมบูรณ์เท่าไรนะครับ เพราะเวลารีเฟรช พวก comment ต่างๆ ที่เคยพิมพ์ไว้มันจะหายไปหมดเลย เราจะแก้ปัญหานี้ด้วยการนำฐานข้อมูลมาใช้ครับ ทุกครั้งที่มีการ comment เราจะต้องเซฟข้อมูล comment นั้นๆ ลงไปในฐานข้อมูลด้วยเสมอ แล้วตอนที่โหลดแอปมาครั้งแรก ก็ให้เราไปดึง comment ทั้งหมดจากฐานข้อมูลมาแสดง ส่วนวิธีการติดต่อกับฐานข้อมูลนั้น ผมแนะนำให้ทำผ่าน API ครับคำถามต่อมาก็คือ แล้วเราจะเอาโค้ดส่วนที่เรียกใช้ API ไปไว้ที่ไหน ? ประเด็นนี้ยังคงเป็นที่ถกเถียงกันอยู่พอสมควรเลยนะครับ แต่ส่วนตัวผมขอแนะนำให้เอาไว้ที่ Action แล้วกัน เพราะรู้สึกว่ามันจะไล่โค้ดได้ง่ายกว่าการเอาไว้ใน Store ครับ เพื่อให้เห็นภาพมากขึ้น เรามาลองเขียนโค้ดต่อยอดจากแอปเดิมกันเลย5.1 เพิ่ม actionTypeเริ่มด้วยการเพิ่ม actionType ให้กับ Action สำหรับการดึงข้อมูลจากฐานข้อมูลมาแสดงก่อนเลยครับ ให้เราเข้าไปเพิ่มได้ที่ DiscussionConstants.jsvar keyMirror = require('keymirror');

module.exports = keyMirror({
DISCUSSION_LOADING: null, // สำหรับบอกว่าแอปกำลังโหลดข้อมูลอยู่
DISCUSSION_CREATE: null,
DISCUSSION_REQUEST: null // สำหรับดึงข้อมูลจาก API มาแสดง
});
แต่จะเห็นว่านอกจาก DISCUSSION_REQUEST แล้ว ผมยังเพิ่ม DISCUSSION_LOADING เข้ามาด้วยนะครับ เราจะใช้ actionType นี้แหละ ในการบอกว่าแอปกำลังอยู่ในสถานะโหลดข้อมูลอยู่

5.2 เพิ่ม Action สำหรับดึงข้อมูลจาก API

เมื่อได้ actionType มาแล้ว เรามาสร้างตัว Action จริงๆ กันเลยครับ ให้เราเพิ่ม method getComments() เข้ามา แล้วก็ปรับ method addComment() เล็กน้อย ตามนี้ครับ// โหลด superagent มาใช้เรียก API
var request = require('superagent');
var DiscussionActions = {// เพิ่ม method สำหรับดึง comment จาก API
// โดยส่งค่าเลขหน้าไปให้
getComments: function(page) {

// บอก dispatcher ว่าเริ่มโหลดข้อมูลแล้วนะ
AppDispatcher.dispatch({
actionType: DiscussionConstants.DISCUSSION_LOADING
});
// ใช้ superagent ดึงข้อมูลจาก API
request
.get('http://www.myapp.com/api/comment?limit=10&page=' + page)
.end(function(err, res){

// เมื่อได้ข้อมูลมาก็บอก dispatcher ว่าได้ข้อมูลมาแล้ว
// พร้อมกับพ่วงข้อมูล comment เข้าไปด้วย
AppDispatcher.dispatch({
actionType: DiscussionConstants.DISCUSSION_REQUEST,
comments: res.body
});
});
},
addComment: function(comment) {
AppDispatcher.dispatch({
actionType: DiscussionConstants.DISCUSSION_CREATE,
comment: comment,
});
// ใช้ superagent เซฟข้อมูล comment ผ่าน API
request
.post('http://www.myapp.com/api/comment')
.send({ comment: comment })
.end(function(err, res){
console.log(res);
});
}

};
ไฮไลท์นั้นอยู่ที่ method getComments() ครับ จะเห็นว่ามาถึงเราจะ dispatch DISCUSSION_LOADING ไปก่อนเลย จากนั้นเราจะเริ่มส่ง ajax request ไปยัง API เพื่อขอข้อมูล comment เมื่อได้รับข้อมูลมาแล้ว เราถึงจะ dispatch DISCUSSION_REQUEST เพื่อส่งข้อมูล comment ที่ได้มาจาก API ไปให้ Store ครับ
ส่วน method addComment() ก็จะคล้ายๆ กันครับ เพียงแต่ตอนส่ง ajax request นั้น เราไม่จำเป็นต้องใส่ callback function อะไรเลยก็ได้ เพราะเราจะใช้วิธี optimistic อยู่แล้ว (คือจะแสดงข้อมูลออกมาทางหน้าจอทันที โดยที่ไม่สนว่าข้อมูลนั้นจะถูกเซฟลงฐานข้อมูลเสร็จแล้วหรือยัง)5.3 สร้าง Method แล้วจับคู่เข้ากับ actionTypeจากนั้นเรามาเพิ่ม method ที่ Store กันครับว่าจะให้ทำอะไรกับข้อมูลที่ได้มา...// เพิ่ม loading และ page เริ่มต้นเข้าไปใน state
var _state = {
loading: false,
page: 1
};
...// private method สำหรับเพิ่ม comment ใหม่เข้าไปต่อท้าย comment เดิม
// พร้อมกับอัพเดท page ให้มีค่าเป็นเลขหน้าถัดไป
function parseData(comments) {
_comments = _comments.concat(comments);
_state.page = _state.page + 1;
}
AppDispatcher.register(function(action) {

// เพิ่มการจับคู่ actionType ใหม่ เข้ากับ method ของ Store
switch (action.actionType) {

// เปลี่ยน loading ให้เป็น true
case DiscussionConstants.DISCUSSION_LOADING:
_state.loading = true;
break;
// เรียกใช้ parseData() พร้อมกับเปลี่ยน loading ให้เป็น false เหมือนเดิม
case DiscussionConstants.DISCUSSION_REQUEST:
parseData(action.comments);
_state.loading = false;
break;

...
}
DiscussionStore.emitChange();
});
ก่อนอื่นให้เราเปลี่ยนค่าเริ่มต้นของ _state ให้มีการเก็บสถานะ loading ก่อน เราจะได้รู้ว่าตอนนี้แอปกำลังโหลดข้อมูลอยู่หรือเปล่า ส่วนอีกค่านึงที่ต้องเก็บลง _state ก็คือ page ซึ่งเราจะเอาไว้บอกแอปว่าจะต้องไปดึง comment หน้าที่เท่าไรมา
เสร็จแล้วเราจะต้องเพิ่ม private method ที่ชื่อ parseData() เข้ามาครับ เราจะใช้ method นี้ล่ะ ในการรวมร่าง comment ที่โหลดมาก่อนหน้าเข้ากับ comment ที่เพิ่งโหลดมาใหม่ รวมไปถึงการอัพเดทค่า page ที่อยู่ใน _state ด้วย เพื่อเป็นการบอกแอปให้คราวหน้าไปดึงข้อมูลของหน้าถัดไปนั่นเองครับเมื่อ method พร้อมแล้ว ทีนี้เราต้องมาจับคู่ให้มันครับ ให้เราจับคู่ DISCUSSION_REQUEST เข้ากับ parseData() ได้เลย จะเห็นว่าผมต้องไปกำหนด loading ให้กลับมาเป็น false เหมือนเดิมด้วยนะครับ เพราะ DISCUSSION_LOADING ที่ถูก dispatch มาก่อนจะเซท loading เป็น true ไว้5.4 เรียกใช้ Action ที่ Viewเนื่องจากผมอยากให้มาถึงก็โหลด comment ล่าสุดมาจากฐานข้อมูลเลย ผมเลยจะขอให้มันไปเรียก getComments() ในตอนที่ componentDidMount เลยละกัน จากนั้นผมก็จะขอเพิ่ม button เข้ามาอันนึง เพื่อเอาไว้กดดู comment เพิ่มเติม เวลามี comment เยอะๆ ครับ จะเห็นว่าปุ่มนี้มันก็ไปเรียก getComments() เหมือนตอนที่ componentDidMount นั่นแหละ เพียงแต่ comment ที่ได้มานั้นจะเป็นของหน้าถัดไปแล้ว เพราะเราจะมีการอัพเดท page ใหม่ทุกครั้งที่โหลด comment เข้ามานั่นเองครับvar DiscussionApp = React.createClass({

...

// มาถึงก็ให้ดึง comment จาก API มาแสดงเลย
componentDidMount: function() {

...
DiscussionActions.getComments(this.state.page);
},

// เพิ่ม method สำหรับดึง comment เพิ่มเติม
_onViewMoreComments: function() {
DiscussionActions.getComments(this.state.page);
},
...// เมื่ออยู่ในสถานะ loading ให้แสดงข้อความ Loading... ด้วย
// เพิ่มปุ่มสำหรับโหลด comment เพิ่มเติม
render: function() {
var comments = DiscussionStore.getAll();
return (
<div>
<DiscussionForm />
<DiscussionList comments={comments} />
{this.state.loading ? <p>Loading...</p> : null}
<button onClick={this._onViewMoreComments}>View more comments</button>
</div>
);
}
});
สังเกตนิดนึงนะครับว่าผมจะมีการเช็คค่า loading ที่อยู่ใน state ด้วย เพราะถ้าแอปกำลังโหลดข้อมูลอยู่ ผมอยากจะให้มันแสดง feedback อะไรบางอย่างให้ user ได้รับรู้
จากนั้นก็รันดูเลยครับ ให้เราลอง comment ดูเยอะๆ แล้วลองเทสปุ่มโหลด comment เพิ่มเติมดู comment จะต้องแสดงต่อเนื่องกันไปอย่างถูกต้องครับ แล้วที่สำคัญก็คือ เมื่อรีเฟรชแล้ว comment จะต้องไม่หายไปหมดเหมือนในตอนแรก เสร็จแล้วครับ! แอป Discussion ของเราความรู้สึกหลังใช้ Fluxอย่างที่บอกนะครับว่า การทำความเข้าใจ Flux นั้น อาจจะต้องใช้เวลานิดนึง แต่ผมรับรองว่าคุ้มค่าแน่นอนครับ การแยกโค้ดออกเป็นส่วนๆ นั้น ถึงแม้ว่าอาจจะดูยุ่งยากในตอนแรกที่เห็น แต่เชื่อเถอะครับว่าพอคล่องๆ แล้ว เราจะจำ pattern ได้เอง แล้วอะไรๆ มันจะง่ายขึ้นเยอะเลยล่ะครับการเขียนแอปโดยใช้ Flux นั้น ทำให้โค้ดมีระเบียบ แยกออกจากกันอย่างชัดเจนมากขึ้นครับ ประโยชน์ที่เห็นได้ชัดๆ เลยก็คือ มันจะช่วยทำให้การไล่โค้ดง่ายขึ้นกว่าเดิมมาก แล้วยิ่งแอปเรามีคนเขียนร่วมกันหลายคน การใช้ Flux นั้นจะช่วยให้เราสามารถแบ่งงานกันได้ง่ายขึ้นด้วยนะครับ เราอาจแบ่งให้คนนึงรับผิดชอบในส่วนของ component และ action ไปเลย ส่วนอีกคนก็อาจจะดูในส่วนของ store ไป อะไรทำนองนี้ครับผมเคยลองแก้แอปที่เดิมเขียนด้วย jQuery ธรรมดา มาเป็น React + Flux นะครับ ความรู้สึกหลังเขียนเสร็จก็คือ การใช้ React + Flux มันเขียนสนุกกว่าอะครับ มาถึงเราก็แค่จัดเตรียม ui ที่เราอยากจะให้ render ณ state ต่างๆ เอาไว้ ที่เหลือเราก็แค่เล่นกับข้อมูลเท่านั้นเองครับ เราไม่ต้องไปสนว่ามันจะเกิดอะไรขึ้นเมื่อข้อมูลมีการเปลี่ยนแปลง ขอแค่ข้อมูลมันถูก แอปของเราก็จะแสดงผลได้อย่างถูกต้องโดยอัตโนมัติครับแนวทางศึกษาต่อก่อนจากกัน ผมอยากจะบอกว่า มันอาจจะไม่จบแค่ Flux นะครับ เพราะในปัจจุบัน มีหลายๆ เจ้า นำไอเดียของ Flux ไปต่อยอดให้มันดีขึ้น มีฟีเจอร์เยอะขึ้น ลดความยุ่งยากซับซ้อนลง โดยตัวที่ผมแนะนำให้ลองไปเล่นดูก็จะมี Alt และ Redux ครับ Alt นี่จะได้ในเรื่องของ docs ที่โอเค และมีคนใช้งานจริงใน production แล้ว ส่วน Redux นั้นน่าสนใจตรงที่มันเพิ่งเกิดใหม่ได้ไม่นาน ทำให้เห็นแนวคิดของ library อื่นๆ หมดแล้วว่าแต่ละเจ้านั้นมีข้อดีข้อเสียยังไง คิดว่าน่าจะใช้เวลาเรียนรู้ไม่นานมากครับ เพราะวิธีคิดนั้นไม่ได้ต่างจาก Flux มากมายอะไรหากมองดีๆ เราจะเห็นนะครับว่า จริงๆ แล้ว Flux มันไม่ได้ยึดติดอยู่กับ tool หรือ framework ตัวไหนเลย เพราะมันเป็นก็เพียงแค่แนวคิดเท่านั้นเอง นั่นหมายความว่าในอนาคต เราอาจจะเห็นการใช้ Flux ร่วมกับ Angular ก็เป็นได้ ก็คิดซะว่าบทความนี้เป็นการปูพื้นฐานเกี่ยวกับ Flux แล้วกันนะครับ แล้วพบกันใหม่บทความหน้าครับ

--

--