[Redux beginner] Rails + Redux + API
Bài đăng này đã không được cập nhật trong 7 năm
Ở bài trước mình đã hướng dẫn khởi tạo reactjs, react-redux trong rails project (Làm quen với Redux trong rails app.). Bài viết này mình sẽ hướng dẫn sử dụng redux tương tác với API, API demo sẽ là Reddit API .
Các package cần thiết
- superagent
Thư viện hỗ trợ thực hiện các HTTP async request:
npm install superagent
. - redux-thunk
Middleware cho phép bạn viết các action creator trả về function, điều này giúp chúng ta dễ dàng dispatch các action theo ý thích. Các function ở trong nhận
dispatch
vàgetStore
làm param.npm install --save redux-thunk
Implement api util request
Ta tạo thêm 1 thư mục utils
, utils
sẽ chứa các file js thực hiện các request đến API.
Tạo util reddit, dưới đây là GET request, lấy về những bài viết trong 1 subreddit.
//app/javascript/packs/utils/reddit_api_util.js
const RedditAPIUtil = {
fetchSubReddit: (subreddit) => {
return request
.get(`https://www.reddit.com/r/${subreddit}.json`)
.then(response => {
return response.body.data.children
})
}
}
Call api bằng redux actions
Tiếp đến, ta sẽ thực hiện gọi api bằng cách tạo ra redux actions.
// app/javascript/packs/actions/reddit_actions.js
import RedditAPIUtil from '../utils/reddit_api_util';
export const REQUEST_POSTS = 'REQUEST_POSTS'
export const RECEIVE_POSTS = 'RECEIVE_POSTS'
export const SELECT_SUBREDDIT = 'SELECT_SUBREDDIT'
export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'
// function này sẽ là function được gọi trong component,
export const fetchPostsIfNeeded = (subreddit) => (dispatch, getState) => {
if (shouldFetchPosts(getState(), subreddit)) {
return dispatch(fetchPosts(subreddit))
}
}
const fetchPosts = (subreddit) => dispatch => {
RedditAPIUtil.fetchSubReddit(subreddit)
.then(response => {
dispatch(requestPosts(response))
})
}
const shouldFetchPosts = (state, subreddit) => {
const { redditReducer } = state
const posts = redditReducer.postsBySubreddit[subreddit]
if (!posts) {
return true
} else if (posts.isFetching) {
return false
} else {
return posts.didInvalidate
}
}
Xử lý data từ actions bằng reducer
Tạo reducer cho reddit actions:
import { combineReducers } from 'redux'
import {
SELECT_SUBREDDIT,
INVALIDATE_SUBREDDIT,
REQUEST_POSTS,
RECEIVE_POSTS
} from '../actions/reddit_actions'
const postsBySubreddit = (state = {}, action) => {
switch (action.type) {
case INVALIDATE_SUBREDDIT:
case RECEIVE_POSTS:
case REQUEST_POSTS:
return Object.assign({}, state, {
[action.subreddit]: posts(state[action.subreddit], action)
})
default:
return state
}
}
const redditReducer = combineReducers({
postsBySubreddit,...
})
export default redditReducer
Tạo root reducer
Mục đích để ta lưu tất cả các reducer vào 1 chỗ, tiện cho việc quản lý và sử dụng.
import { combineReducers } from 'redux';
import helloReducer from './hello_reducer.js';
import redditReducer from './reddit_reducers';
export default combineReducers({
helloReducer,
redditReducer
})
Cấu hình lại store để sử dụng được ajax request
// app/javascript/packs/configureStore.js
import {createStore, applyMiddleware, compose} from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/root_reducer';
const configureStore = (preloadedState = {}) => {
return compose(applyMiddleware(thunk))(createStore)(rootReducer);
}
export default configureStore
Như vậy về cơ bản phần gọi và xử lý dữ liệu đã hoàn tất, giờ chúng ta sẽ đổ data ra view bằng các component.
Tạo các component
- Pickers - subreddit select box
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class Picker extends Component {
render() {
const { value, onChange, options } = this.props
return (
<span>
<h1>{value}</h1>
<select onChange={e => onChange(e.target.value)} value={value}>
{options.map(option => (
<option value={option} key={option}>
{option}
</option>
))}
</select>
</span>
)
}
}
Picker.propTypes = {
options: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired
}
- Posts - display a list of post
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class Posts extends Component {
render() {
return (
<ul>
{this.props.posts.map((post, i) => <li key={i}>{post.title}</li>)}
</ul>
)
}
}
Posts.propTypes = {
posts: PropTypes.array.isRequired
}
- MainPage - component chính chứa Picker và Posts component
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import * as RedditActions from '../actions/reddit_actions'
import Picker from './Picker'
import Posts from './Posts'
class MainPage extends Component {
constructor(props) {
super(props)
this.handleChange = this.handleChange.bind(this)
this.handleRefreshClick = this.handleRefreshClick.bind(this)
}
componentDidMount() {
const { dispatch, selectedSubreddit, actions: {fetchPostsIfNeeded} } = this.props
fetchPostsIfNeeded(selectedSubreddit)
}
componentDidUpdate(prevProps) {
if (this.props.selectedSubreddit !== prevProps.selectedSubreddit) {
const { dispatch, selectedSubreddit, actions: {fetchPostsIfNeeded} } = this.props
fetchPostsIfNeeded(selectedSubreddit)
}
}
handleChange(nextSubreddit) {
this.props.actions.selectSubreddit(nextSubreddit)
this.props.actions.fetchPostsIfNeeded(nextSubreddit)
}
handleRefreshClick(e) {
e.preventDefault()
const { dispatch, selectedSubreddit,
actions: {invalidateSubreddit, fetchPostsIfNeeded} } = this.props
invalidateSubreddit(selectedSubreddit)
fetchPostsIfNeeded(selectedSubreddit)
}
render() {
const { selectedSubreddit, posts, isFetching, lastUpdated } = this.props
return (
<div>
<Picker
value={selectedSubreddit}
onChange={this.handleChange}
options={['reactjs', 'frontend']}
/>
<p>
{lastUpdated &&
<span>
Last updated at {new Date(lastUpdated).toLocaleTimeString()}.
{' '}
</span>}
{!isFetching &&
<a href="#" onClick={this.handleRefreshClick}>
Refresh
</a>}
</p>
{isFetching && posts.length === 0 && <h2>Loading...</h2>}
{!isFetching && posts.length === 0 && <h2>Empty.</h2>}
{posts.length > 0 &&
<div style={{ opacity: isFetching ? 0.5 : 1 }}>
<Posts posts={posts} />
</div>}
</div>
)
}
}
MainPage.propTypes = {
selectedSubreddit: PropTypes.string.isRequired,
posts: PropTypes.array.isRequired,
isFetching: PropTypes.bool.isRequired,
lastUpdated: PropTypes.number,
}
const mapStateToProps = (state) => {
const { selectedSubreddit, postsBySubreddit } = state.redditReducer
const {
isFetching,
lastUpdated,
items: posts
} = postsBySubreddit[selectedSubreddit] || {
isFetching: true,
items: []
}
return {
selectedSubreddit,
posts,
isFetching,
lastUpdated
}
}
const mapDispatchToProps = dispatch => ({
actions: bindActionCreators(RedditActions, dispatch)
})
export default connect(mapStateToProps, mapDispatchToProps)(MainPage)
- Root component
// app/javascripts/packs/components/Root.js
import React, { Component } from 'react'
import { Provider } from 'react-redux'
import configureStore from '../configureStore'
import MainPage from './MainPage'
const store = configureStore()
export default class Root extends Component {
render() {
return (
<Provider store={store}>
<MainPage />
</Provider>
)
}
}
- Khai báo lại component với rails
// app/javascripts/packs/application.js
import React from 'react'
import { render } from 'react-dom'
import Root from './components/Root'
render(
<Root />,
document.getElementById('root')
)
Kết thúc
Như vậy chúng ta đã hoàn thiện 1 rails app cùng webpacker gem tương tác với api bên ngoài và giúp cho ta dễ dàng sử dụng các thư viện javascript trong rails. Bài viết dựa trên tutorial chính thức của redux.js.org : http://redux.js.org/docs/advanced/ExampleRedditAPI.html Github của tutorial: https://github.com/nguyenthetoan/RedditRails
All rights reserved