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ý 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 LoopbackMysql
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 [email protected] 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:


All Rights Reserved