[Redux beginner] Rails + Redux + API
Ở 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 ...
Ở 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