Redux cho người mới bắt đầu - Part 3 Middleware

Mở đầu

Chào tất cả mọi người, chúng ta lại gặp nhau trong bài viết thứ 3 của series Redux cho người mới bắt đầu sau 1 thời gian rất rất dài (vì đứa viết bài lười quá (yaoming) ). Ở bài viết số 2, chúng ta đã làm thử một ứng dụng To-Do đơn giản. Trong thực tế, ứng dụng của chúng ta còn cần giải quyết nhiều vấn đề hơn như ghi log hoạt động, handle error, async... Để giải quyết những vấn đề này, Redux đã đưa ra giải pháp gì, chúng ta sẽ tìm hiểu trong bài viết này.

Middleware

Middleware khá phổ biến với các Framework server-side, nó được đặt giữa thời điểm server nhận request và thời điểm server response. Ở Redux, Middleware giải quyết vấn đề khác với các Framework server-side nhưng định nghĩa có phần tương tự:

It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.

Hiểu một cách đơn giản middleware cho phép chúng ta can thiệp vào giữa thời điểm dispatch một action và thời điểm action đến được reducer. Chúng ta có thể thấy sự thay đổi của flow khi có sử dụng middleware qua hình dưới:

Sử dụng Middleware

Để sử dụng được Middleware chúng ta cần sử dụng function applyMiddleware của redux khi khởi tạo store

// index.js
import { createStore, applyMiddleware } from 'redux';
import 'yourMiddleware' from 'your-middleware';
import rootReducer from './reducers/rootReducer';

const store = createStore(rootReducer, applyMiddleware(yourMiddleware));

À thế cái yourMiddleware kia ở đâu ra ? Tất nhiên là chúng ta sẽ tự viết, hoặc cũng có thể dùng những thư viện middleware xây dựng sẵn. Để có thể tự viết được một Middleware cho redux, chúng ta cần hiểu bản chất Middleware mà Redux cung cấp. Chúng ta sẽ đề cập đến nó trong một phần khác, còn trong phần này cứ dùng thử một vài thư viện đã.

Middleware ways

Hiện tại, có khá nhiều thư viện middleware cho Redux, cá nhân mình thì thấy có 3 thư viện chúng ta nên thử qua redux-thunk redux-sagaredux-observable . Mỗi thư viện có phương pháp giải quyết vấn đề side effects riêng. Nhưng các bạn yên tâm hôm nay chúng ta sẽ chỉ thử duy nhất 1 thư viện thôi (yaoming) đó là redux-thunk thư viện được giới thiệu bởi Redux và cũng là cái dễ hiểu nhất.

Redux-Thunk: Function Action

“Redux Thunk middleware allows you to write action creators that return a function instead of an action. The thunk can be used to delay the dispatch of an action, or to dispatch only if a certain condition is met. The inner function receives the store methods dispatch and getState as parameters.” — Redux Thunk documentation

Khá là dễ hiểu, thunk cho phép chúng ta viết actionfunction thay vì bắt buộc là object như định nghĩa action mà Redux đưa ra. Hơi khó tưởng tượng ? chúng ta demo thử cái xem sao.

Demo

Trước hết, chúng ta cứ chuẩn bị một project mới đã. Lần này chúng ta sẽ làm một ứng dụng load dữ liệu.

create-react-app redux_middleware_ways
cd redux_middleware_ways
yarn add redux react-redux redux-thunk redux-saga redux-observable 
cd src
mkdir reducers
touch reducers/index.js reducers/dataReducer.js
touch configureStore.js constants.js actions.js
// constants.js
export const FETCHING_DATA = 'FETCHING_DATA'
export const FETCHING_DATA_SUCCESS = 'FETCHING_DATA_SUCCESS'
export const FETCHING_DATA_FAILURE = 'FETCHING_DATA_FAILURE'
// actions.js
import { FETCHING_DATA, FETCHING_DATA_SUCCESS, FETCHING_DATA_FAILURE } from './constants'

export function getData() {
  return {
    type: FETCHING_DATA
  }
}

export function getDataSuccess(data) {
  return {
    type: FETCHING_DATA_SUCCESS,
    data,
  }
}

export function getDataFailure() {
  return {
    type: FETCHING_DATA_FAILURE
  }
}

export function fetchData() {}
// reducer/dataReducer.js
import { FETCHING_DATA, FETCHING_DATA_SUCCESS, FETCHING_DATA_FAILURE } from '../constants'
const initialState = {
  data: [],
  dataFetched: false,
  isFetching: false,
  error: false
}

export default function dataReducer (state = initialState, action) {
  switch (action.type) {
    case FETCHING_DATA:
      return {
        ...state,
        data: [],
        isFetching: true
      }
    case FETCHING_DATA_SUCCESS:
      return {
        ...state,
        isFetching: false,
        data: action.data
      }
    case FETCHING_DATA_FAILURE:
      return {
        ...state,
        isFetching: false,
        error: true
      }
    default:
      return state
  }
}
// reducer/index.js
import { combineReducers } from 'redux'
import appData from './dataReducer'

const rootReducer = combineReducers({
    appData
})

export default rootReducer
// App.js
import React from 'react'

import { connect } from 'react-redux'
import { fetchData } from './actions'

let styles

const App = (props) => {
  const {container, text, button, buttonText} = styles

  return (
    <div style={container}>
      <div style={text}>Redux Examples</div>
      <div style={button}>
        <div style={buttonText}>Load Data</div>
      </div>
    </div>
  )
}

styles = {
  container: {
    marginTop: 100
  },
  text: {
    textAlign: 'center'
  },
  button: {
    display: 'flex',
    minHeight: 60,
    margin: 10,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#0b7eff'
  },
  buttonText: {
    color: 'white'
  }
}

function mapStateToProps (state) {
  return {
    appData: state.appData
  }
}

function mapDispatchToProps (dispatch) {
  return {
    fetchData: () => dispatch(fetchData())
  }
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(App)

// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';

import configureStore from './configureStore'
const store = configureStore()

import App from './App';

const ReduxApp = () => (
  <Provider store={store}>
    <App />
  </Provider>
);

ReactDOM.render(
  <ReduxApp />,
  document.getElementById('root')
);
`npm start` 

Khâu chuẩn bị đã xong. À khoan chúng ta còn cần 1 api để load dữ liệu nữa. Respone chậm một chút để nhận thấy sự thay đổi.

touch api.js
// api.js
const people = [
  { name: 'Nader', age: 36 },
  { name: 'Amanda', age: 24 },
  { name: 'Jason', age: 44 }
]

export default () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      return resolve(people)
    }, 3000)
  })
}

Để sử dụng thunk chúng ta sẽ chỉ cần thêm một vài dòng code:

// configureStore.js
import { createStore, applyMiddleware } from 'redux'
import rootReducer from './reducers'
import thunk from 'redux-thunk' //import thunk

export default function configureStore() {
  let store = createStore(rootReducer, applyMiddleware(thunk)) // create store sử dụng thunk
  return store
}
// actions.js
//...
import getPeople from './api'
export function fetchData() {
  return (dispatch) => {
    dispatch(getData())
    getPeople()
      .then((data) => {
        dispatch(getDataSuccess(data))
      })
      .catch((err) => console.log('err:', err))
  }
}

Khi action fetchData được gọi, nó trả về một function nhận dispatch làm tham số, function sẽ dispatch getData action. Tiếp theo function getPeople sẽ được gọi. Sau khi getPeople hoàn thành sẽ dispatch getDataSuccess action. Action fetchData bây giờ đã trở thành một thunk.

Thunk là một function được bao lại để tạm dừng nó cho đến khi được gọi

Chúng ta cũng cần thay đổi view một chút:

// app.js
const App = (props) => {
  const {container, text, button, buttonText} = styles

  return (
    <div style={container}>
      <div style={text}>Redux Examples</div>
      <div style={button} onClick={() => props.fetchData()}>
        <div style={buttonText}>
          {
            props.appData.isFetching && <div>Loading</div>
          }
          {
            props.appData.data.length ? (
              props.appData.data.map((person, i) => {
                return <div key={i} >
                  <div>Name: {person.name}</div>
                  <div>Age: {person.age}</div>
                </div>
              })
            ) : null
          }
        </div>
      </div>
    </div>
  )
}

Giờ thì thử click vào nút màu xanh và xem kết quả. Vậy là chúng ta đã sử dụng được redux-thunk khá đơn giản. Điểm khác biệt duy nhất là:

Chúng ta có thể viết action là một function thay vì bắt buộc là object

Ưu điểm:

  • Redux-thunk không yêu cầu chúng ta phải hiểu thêm concept nào của riêng nó để có thể sử dụng, tất cả vẫn là Redux. Ý tưởng cơ bản của Thunk là nếu cần kích hoạt side effects, hãy dùng chính actions. Một function trả về một function có thể thực hiện mọi thứ mà async call cần và dispatch bất kì action nào ta muốn.

Nhược điểm:

  • Chính ý tưởng của Thunk sớm dẫn nó tới sự phức tạp về khả năng test actions, do chỉ có thể biết các thunk làm gì cho đến khi nó được thực thi. Chúng ta vẫn có thể test với mocks, nhưng điều đó sẽ phá vỡ các quy tắc functional programming của Redux.

Kết

Chúng ta đã cùng nhau điểm qua khái niệm và sử dụng một middleware đơn giản. Không quá nhiều nhưng chúng ta cũng đã có được những cái nhìn đầu tiên. Middleware là một khái niệm khá thú vị của Redux. Nó hoạt động ra sao, làm thế nào để tự viết được một Middleware hay những thư viện Middleware xuất hiện sau nó đều là những thứ hay ho đáng xem qua. Hẹn gặp lại mọi người trong phần tiếp theo (hi vọng là không xa lắm (yaoming) )