Xây dựng ứng dụng đơn giản với React, Redux sagas
Tổng quan Bài viết này mình sẽ xây dựng một ứng dụng đăng ký, đăng nhập sử dụng React để làm phía frontend và sử dụng Api viết với Loopback mình đã làm ở bài trước https://viblo.asia/p/xay-dung-api-cho-ung-dung-xac-thuc-nguoi-dung-nhanh-chong-voi-strongloops-loopback-m68Z0wY6KkG Về luồng xử lý ...
Tổng quan
Bài viết này mình sẽ xây dựng một ứng dụng đăng ký, đăng nhập sử dụng React để làm phía frontend và sử dụng Api viết với Loopback mình đã làm ở bài trước https://viblo.asia/p/xay-dung-api-cho-ung-dung-xac-thuc-nguoi-dung-nhanh-chong-voi-strongloops-loopback-m68Z0wY6KkG
Về luồng xử lý khá giản, có 2 màn hình chính là form đăng ký và đăng nhập 1. Màn hình đăng ký 1. Màn hình đăng nhập
Bài viết này được tham khảo từ https://start.jcolemorrison.com/react-and-redux-sagas-authentication-app-tutorial/ là một bài viết rất hay, mình khuyên các bạn nên đọc khi muốn tìm hiểu về Docker, React, Redux, Sagas và kiến trúc Single Page Application
Cài đặt Api
Ở bài trước mình đã build các Image authapi_db và authapi_api bằng Loopback và Mysql
Các bạn có thể xem hướng dẫn cụ thể tại https://viblo.asia/p/xay-dung-api-cho-ung-dung-xac-thuc-nguoi-dung-nhanh-chong-voi-strongloops-loopback-m68Z0wY6KkG
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES afbab4a459b2 authapi_db "docker-entrypoint..." 34 minutes ago Up 25 minutes 0.0.0.0:3306->3306/tcp authapi_db_1 4a90fda25faf authapi_api "nodemon ." 34 minutes ago Up 25 minutes 0.0.0.0:3001->3000/tcp authapi_api_1
Khởi tạo ứng dụng React với create-react-app
$ npm install -g create-react-app $ create-react-app . $ yarn add redux react-redux redux-saga react-router@3 redux-form
Cấu trúc thư mục sẽ như thế này:
src ├── lib │ ├── api-errors.js │ └── check-auth.js ├── client │ ├── actions.js │ ├── constants.js │ └── reducer.js ├── login │ ├── actions.js │ ├── constants.js │ ├── index.js │ ├── reducer.js │ └── sagas.js ├── signup │ ├── actions.js │ ├── constants.js │ ├── index.js │ ├── reducer.js │ └── sagas.js └── widgets │ ├── actions.js │ ├── constants.js │ ├── index.js │ ├── reducer.js │ └── sagas.js ├── notifications │ ├── Errors.js │ └── Messages.js ├── App.css ├── App.js ├── App.test.js ├── logo.svg ├── index.js ├── index-reducer.js ├── index-sagas.js ├── registerServiceWorker.js
Trong đó:
- index.js - chính là component, container là cốt lỗi của ứng dụng được viết bằng React
- sagas.js - nơi chúng ta viết các tác vụ bất đồng bộ và các request để tương tác với server api
- reducer.js - nơi quản lý các state của component, container đó
- actions.js - nơi viết tất cả các actions mà component, container gửi đi
- constants.js - nơi lưu trữ tất cả các constants reducers/actions
// src/index.js import React from 'react' import ReactDOM from 'react-dom' import { Router, Route, browserHistory } from 'react-router' import { Provider } from 'react-redux' import { createStore, compose, applyMiddleware } from 'redux' import createSagaMiddleware from 'redux-saga' import App from './App' import Login from './login' import Signup from './signup' import Widgets from './widgets' import IndexReducer from './index-reducer' import IndexSagas from './index-sagas' import registerServiceWorker from './registerServiceWorker' const sagaMiddleware = createSagaMiddleware() const composeSetup = process.env.NODE_ENV !== 'production' && typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose const store = createStore( IndexReducer, composeSetup(applyMiddleware(sagaMiddleware)) ) sagaMiddleware.run(IndexSagas) ReactDOM.render( <Provider store={ store }> <Router history={ browserHistory }> <Route path='/' component={ App }> <Route path='/login' component={ Login } /> <Route path='/signup' component={ Signup } /> <Route path='/widgets' component={ Widgets } /> </Route> </Router> </Provider>, document.getElementById('root') ) registerServiceWorker()
// src/index-reducer.js import { combineReducers } from 'redux' import { reducer as form } from 'redux-form' const IndexReducer = combineReducers({ form, }) export default IndexReducer
// src/index-sagas.js import SignupSaga from './signup/sagas' const IndexSagas = function* (){ yield [] } export default IndexSagas
# .env REACT_APP_API_URL=http://localhost:3001
// src/signup/index.js import React, { Component } from 'react' class Signup extends Component {} export default Signup
// src/login/index.js import React, { Component } from 'react' class Login extends Component {} export default Login
// src/widgets/index.js import React, { Component } from 'react' class Widgets extends Component {} export default Widgets
// src/App.js import React, { Component } from 'react' import './App.css' class App extends Component { render() { return ( <div className="App"> <div className="App-intro"> { this.props.children } </div> </div> ) } } export default App
Quản lý state cho clients
// src/clients/actions import { CLIENT_SET, CLIENT_UNSET } from './constants' export function setClient(token){ return { type: CLIENT_SET, token } } export function unsetClient(){ return { type: CLIENT_UNSET { }
// src/clients/constants.js export const CLIENT_SET = 'CLIENT_SET' export const CLIENT_UNSET = 'CLIENT_UNSET'
// src/clients/reducer.js import { CLIENT_SET, CLIENT_UNSET } from './constants' const initialState = { id: null, token: null } function clientReducer(state = initialState, action){ switch(action.type){ case CLIENT_SET: return { id: action.token.userId, token: action.token } case CLIENT_UNSET: return { id: null, token: null } default: return state } } export default clientReducer
// src/index-reducer.js import { combineReducers } from 'redux' import { reducer as form } from 'redux-form' import client from './client/reducer' const IndexReducer = combineReducers({ client, }) export default IndexReducer
Quản lý state cho signup
// src/signup/actions.js import { SIGNUP_REQUESTING, } from './constants' function requestSignup({ email, password }){ return { type: SIGNUP_REQUESTING, email, password } } export default requestSignup
// src/signup/reducer.js import { SIGNUP_REQUESTING, SIGNUP_SUCCESS, SIGNUP_ERROR } from './constants' const initialState = { requesting: false, success: false, errors: [], messages: [] } function signupReducer(state=initialState, action){ switch(action.type){ case SIGNUP_REQUESTING: return { requesting: true, success: false, errors: [], messages: [{ body: 'Signing up...', time: new Date() }] } case SIGNUP_SUCCESS: return{ requesting: false, success: true, errors: [], messages: [{ body: `Signed up successfull for ${ action.response.email }`, time: new Date() }] } case SIGNUP_ERROR: return{ requesting: false, success: false, errors: state.errors.concat({ body: action.error.toString(), time: new Date() }), messages: [] } default: return state } } export default signupReducer
// src/index-reducer.js import { combineReducers } from 'redux' import { reducer as form } from 'redux-form' import client from './client/reducer' import signup from './signup/reducer' const IndexReducer = combineReducers({ client, signup, form, }) export default IndexReducer
View cho signup
// src/signup/index.js import React, { Component } from 'react' import { Link } from 'react-router' import { Field, reduxForm } from 'redux-form' import { connect } from 'react-redux' import Messages from '../notifications/Messages' import Errors from '../notifications/Errors' import requestSignup from './actions' class Signup extends Component{ submit = (values) => { this.props.requestSignup(values) } render(){ const { requesting, success, errors, messages } = this.props.signup return ( <div className="signup"> <form className="widget-form" onSubmit={ this.props.handleSubmit(this.submit) }> <h1>Signup</h1> <label htmlFor="email">Email</label> <Field name="email" type="email" id="email" className="email" label="Email" component="input" /> <label htmlFor="password">Password</label> <Field name="password" type="password" id="password" className="email" label="Password" component="input" /> <button action="submit">SIGNUP</button> </form> <div className="auth-messages"> { !!messages.length && (<Messages messages={ messages } />)} { !!errors.length && (<Errors errors={ errors } />) } { !requesting && ( <div>Please login: <Link to="/login">Login</Link></div> ) } </div> </div> ) } } const mapStateToProps = state => ({ signup: state.signup }) const connected = connect(mapStateToProps, { requestSignup })(Signup) const formed = reduxForm({ form: 'signup', })(connected) export default formed
Viết các api tương tác với server và quản lý bất động bộ với Redux Sagas
// src/signup/sagas.js import { takeLatest, call, put } from 'redux-saga/effects' import { handleApiErrors } from '../lib/api-errors' import { SIGNUP_REQUESTING, SIGNUP_SUCCESS, SIGNUP_ERROR } from './constants' const signupUrl = `${ process.env.REACT_APP_API_URL }/api/clients` function signupApi(email, password){ return fetch(signupUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }) }) .then(handleApiErrors) .then(response => response.json()) .then(json => json) .catch((error) => { throw error }) } function* signupFlow(action){ const { email, password } = action try { const response = yield call(signupApi, email, password) yield put({ type: SIGNUP_SUCCESS, response }) } catch(error){ yield put({ type: SIGNUP_ERROR, error }) } } function* signupWatcher(){ yield takeLatest(SIGNUP_REQUESTING, signupFlow) } export default signupWatcher
// src/index-sagas.js import SignupSaga from './signup/sagas' export default function* IndexSaga () { yield [ SignupSaga(), ] }
Kết luận
Vậy là đã hoàn thành một ứng dụng xác thực người dùng với chức năng Signup. Mình rất muốn viết hết các Login và quản lý Widget nhưng không có nhiều thời gian để thực hiện. Nhưng về cơ bản thì bài viết này đã có đủ các thành phần để các bạn có thể hiểu về React, Redux, Sagas là gì. Chúc các bạn một tuần mới nhiều năng lượng và làm việc hiệu quả. HappyCoding :upside_down: