Học React/Redux qua ví dụ thực tế: Sử dụng dữ liệu từ SoundCloud API và kết thúc hành trình

Cuộc vui đã gần đi đến hồi kết, trong bài viết lần trước, tôi đã nói là còn khoảng hai bài viết nữa mới kết thúc series, tuy nhiên tôi đã drop một vài tuần, à không nói cho chính xác thì phải là một vài tháng vì những công chuyện cá nhân. Chính vì thế mà hôm nay tôi sẽ đem đến cho các bạn một bài viết dài hơn bình thường để kết thúc series này, và chuẩn bị cho những bài viết tiếp theo.

Nào mình cùng lên xe buýt, à lộn, nào mình cùng bắt đầu!

Fetch đống dữ liệu về mà xài

Đầu tiên, làm gì nhỉ? À hiện tại chúng ta đã authenticated vào SoundCloud API được rồi, vậy thì từ đó chúng ta cứ thế lấy data về xài thôi. Ủa mà xài sao nhỉ?

Đọc API docs của SoundCloud xem thử tải danh sách bài hát của user là api nào chứ sao, sau khi authentication xong thì gọi API đó và set data thông qua action setTracks. Các bạn khỏi đọc cũng được vì tôi đã đọc rồi, hehe.

Mở actions/auth.js và thêm vào các thành phần để lấy danh sách bài hát như sau.

...
import {setTracks} from '../actions/track';
...
export function auth() {
return (dispatch) => {
SoundCloud.connect().then((session) => {
dispatch(fetchMe(session));
dispatch(fetchTracks(session));
});
}
}
...
function fetchTracks(session) {
return (dispatch) => {
fetch(`${API_HOST}/me/activities?limit=20&offset=0&oauth_token=${session.oauth_token}`)
.then((res) => res.json())
.then((data) => {
dispatch(setTracks(data.collection));
});
}
}
view raw auth.js hosted with ❤ by GitHub

Như các bạn thấy, tôi gọi API activities và lấy 20 phần tử đầu tiên. Chúng ta sẽ fetchTracks asynchronous với việc fetchMe (lấy thông tin người dùng) sau khi authentication thành công.

Và vì dữ liệu từ API thực tế trả về sẽ khác với cái mà chúng ta hard code ngay từ đầu, nên chúng ta cũng sẽ phải chỉnh sửa presenter của TrackList cho đúng. Và để tiện lợi hơn, tôi sẽ tách thành phần hiển thị track ra một component mới mang tên Track.

Tạo file components/TrackList/Track.js.

import React, {PropTypes, Component} from 'react';
export default class Track extends Component {
static propTypes = {
track: PropTypes.object.isRequired
}
render() {
const {origin: {artwork_url, title}} = this.props.track
return (
<div className='track'>
<img src={artwork_url} />
{title}
<button type='button'>Play</button>
</div>
)
}
}
view raw Track.js hosted with ❤ by GitHub

Như các bạn thấy, component này nhận vào prop là track, và hiển thị art work cũng như title của track, ngoài ra thì chúng ta cũng làm sẵn nút play để dành cho tiết mục tiếp theo.

À đừng quên viết test cho component này nhé để chắc chắn là code của chúng ta sẽ hoạt động với dữ liệu lấy từ SoundCloud API, tạo components/TrackList/Track.spec.js. Test những gì thì tôi có gợi ý như là hiển thị đúng format, hiển thị đúng title, hiển thị đúng art work, etc. Chi tiết cách viết các bạn có thể tìm hiểu qua bài Testing nhé!

Ví dụ test case tôi viết như sau.

import Track from './Track';
import {shallow} from 'enzyme';
describe('Track', () => {
it('shows track title', () => {
const props = {
track: {origin: {title: 'foo'}}
}
const element = shallow(<Track {...props} />);
expect(element.contains('foo')).to.be.true;
})
});
view raw Track.spec.js hosted with ❤ by GitHub

import TrackList from './TrackList';
import Track from './Track';
import {shallow} from 'enzyme';
describe('TrackList', () => {
it('shows two <Track /> component', () => {
const props = {
tracks: {
tracks: [{ origin: { id: 1, title: 'foo' }}, { origin: { id: 2, title: 'bar' }}],
activeTrack: null
}
};
const element = shallow(<TrackList {...props} />);
expect(element.find(Track)).to.have.length(2);
});
});

Rồi cùng start app xem thử có gì thay đổi không nhé!

screen-shot-2017-02-27-at-12-33-07-am
Đây là kết quả

Music Player

Okay, giờ chúng ta sẽ tiếp tục tìm các làm cho nhạc được phát khi nút Play được click vào.

Đầu tiên hãy cũng phân tích vấn đề. Chúng ta phải chơi nhạc khi mà nút Play được click. Vì thế chúng ta phải có một function handle callback khi nút Play được bấm, function này sẽ nằm ở đâu?

Để đơn giản nhất chúng ta sẽ sử dụng tag Audio của HTML5, với tag này chúng ta chỉ cần đưa vào source mà trigger hàm play đã được gắn sẵn. Và dĩ nhiên chúng ta không nên để tag này trong mỗi Track được vì nếu thế sẽ có rất nhiều tag audio được sinh ra mà rất khó control, vì thế nó sẽ phải nằm ở higher order component là TrackList.

export default class TrackList extends Component {
render() {
return (
<div>
...
<div>
<audio
id='player'
ref={(player) => {this.player = player;}}
src='iDontKnow'>
</audio>
</div>
</div>
)
}
}
view raw TrackList.js hosted with ❤ by GitHub

Ok, như vậy chúng ta đã có cái sườn đầu tiên, câu hỏi tiếp theo, làm sao chúng ta biết track nào đang được chơi?

Như vậy, chúng ta phải có một cái state để chứa track đang được chơi, tạm gọi là activeTrack, state này sẽ nên nằm trong tracks cho đồng bộ, như thế ta có.

import {ActionTypes} from '../core/constants';
const initialState = {
tracks: [],
activeTrack: null
};
export default function(state = initialState, action) {
switch (action.type) {
case ActionTypes.TRACK_PLAY:
return setActiveTrack(state, action)
}
return state;
}
function setActiveTrack(state, action) {
const {track} = action;
return {...state, activeTrack: track};
}
view raw tracks.js hosted with ❤ by GitHub

Và để trigger làm cho state này thay đổi, tất nhiên chúng ta không thể thiếu action playTrack.

import {ActionTypes} from '../core/constants';
...
export function play(track) {
return {
type: ActionTypes.TRACK_PLAY,
track
};
}
view raw tracks.js hosted with ❤ by GitHub

Và vâng, đây chính là hàm play mà chúng ta đã nhắc tới lúc đầu, action giúp change state activeTrack sẽ đảm nhận nhiệm vụ handle callback khi mà nút Play được click vào.

Hãy thêm hàm play vào phần @connect của TrackList và pass nó xuống Track nhé.

import React, {PropTypes, Component} from 'react';
export default class Track extends Component {
static propTypes = {
track: PropTypes.object.isRequired,
play: PropTypes.func.isRequired
}
_play() {
this.props.play(this.props.track);
}
render() {
const {origin: {artwork_url, title}} = this.props.track
return (
<div className='track'>
<img src={artwork_url} />
{title}
<button onClick={() => this._play()} type='button'>Play</button>
</div>
)
}
}
view raw track.js hosted with ❤ by GitHub

Tiếp theo đó là lục ra xem streaming url của track nằm ở đâu trong đống response của SoundCloud API. Thôi các bạn khỏi tìm, tôi đã tìm ra nó rồi đây, nó sẽ nằm trong origin.stream_url.

Để cho bản nhạc được chơi, như tôi đã nói lúc đầu audio tag có hàm built-in để chúng ta có thể làm việc này. Và lưu ý, là chúng ta chỉ có thể gọi hàm play của audio tag khi mà nó đã render xong nhé. Và để làm được điều này chúng ta cần để ý đến Lifecycle của component trong React. Ở đây chúng ta sẽ phải dùng componentDidUpdate để chắc chắn ràng tất cả mọi thay đổi đã được render xong.

import React, {Component, PropTypes} from 'react';
import Track from './Track';
export default class TrackList extends Component {
static propTypes = {
tracks: PropTypes.object,
user: PropTypes.object,
clientId: PropTypes.string,
auth: PropTypes.func,
play: PropTypes.func
}
static defaultProps = {
tracks: []
}
componentDidUpdate() {
const player = this.player;
if (player) {
if (this.props.tracks.activeTrack) {
player.play();
} else {
player.pause();
}
}
}
render() {
const {tracks: {activeTrack, tracks}} = this.props;
return (
<div>
<div>
{
this.props.user
? <div>Hello {this.props.user.full_name}</div>
: <button onClick={this.props.auth} type='button'>Login</button>
}
</div>
<div>
{
tracks.map((track, key) => {
return (
<Track key={key} track={track} play={this.props.play} />
)
})
}
</div>
<div>
{activeTrack && (
<audio
id='player'
ref={(player) => {this.player = player;}}
src={`${activeTrack.origin.stream_url}?client_id=${clientId}`}>
</audio>
)}
</div>
</div>
)
}
}
view raw TrackList.js hosted with ❤ by GitHub

Giờ thì run app và tận hưởng âm nhạc thôi! Hehe!

Source code trong bài các bạn có thể tìm thấy ở https://github.com/codeaholicguy/react-redux-tutorial/tree/master/fetch-tracks

Hy vọng các bạn đã có những phút giây thư giãn thoải mái, code cưỡi ngựa xem hoa nhưng vẫn nắm đủ những kiến thức cần thiết để tiếp tục tìm hiểu những vấn đề sâu hơn.

Đến đây cũng là hết series, hẹn gặp lại các bạn lần sau. À quên nữa, rất mong các bạn có thể nhắn cho tôi biết những chủ đề mà các bạn mong muốn được tìm hiểu qua fanpage (nhìn bên tay phải là thấy) để chúng ta có thể cùng chia sẻ với nhau qua những bài viết tiếp theo nhé.

Thân ái và quyết thắng hehe!

Series được tham khảo từ: https://www.robinwieruch.de/the-soundcloud-client-in-react-redux/

Một suy nghĩ 2 thoughts on “Học React/Redux qua ví dụ thực tế: Sử dụng dữ liệu từ SoundCloud API và kết thúc hành trình

Bình luận