images
29/08/2021 01:32 am

Redux middleware là gì?

Hiểu một cách đơn giản Redux 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.


Giả sử trong trường hợp, bạn cần ngăn user tạo các article chứa từ khóa bị cấm. Hãy xem hàm handleSubmit trong Form.js

handleSubmit(event) {

    event.preventDefault();

    const { title } = this.state;

    const forbiddenWords = ['spam', 'money'];

    const foundWord = forbiddenWords.filter(word => title.includes(word) )

    if (foundWord) {

      return this.props.titleForbidden();

    }

    this.props.addArticle({ title });

    this.setState({ title: "" });

  }

Không phải mục đích của Redux là chuyển logic ra khỏi React component hay sao?

Rõ ràng chúng ta muốn một thứ gì đó khác biệt. Hiểu một cách đơn giản Redux 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:


Redux middleware là một hàm trả về một hàm, nhận next là tham số. Hàm con sẽ trả về một hàm khác, nhận action là tham số và cuối cùng trả về next(action).

function forbiddenWordsMiddleware() {

  return function(next){

    return function(action){

      // do your stuff

      return next(action);

    }

  }

}

Điều quan trọng là luôn luôn gọi next(action) trong middleware của bạn. Nếu không, Redux sẽ dùng lại do không có một action nào được đưa tới reducer.

Khá khó hiểu phải không? Phần tiếp theo sẽ giải thích rõ hơn.

Thêm Redux middle vào app của bạn

Tạo một thư mục mới cho middleware:

mkdir -p src/js/middleware

Tại file mới, src/js/middleware/index.js:

import { ADD_ARTICLE } from '../constants/action-types';


const forbiddenWords = ['spam', 'money'];


export function forbiddenWordsMiddleware({ dispatch }) {

  return function (next) {

    return function (action) {

      // do your stuff

      if (action.type === ADD_ARTICLE) {

        const foundWord = forbiddenWords.filter((word) =>

          action.payload.title.includes(word)

        );


        if (foundWord.length) {

          return dispatch({ type: 'FOUND_BAD_WORD' });

        }

      }

      return next(action);

    };

  };

}

Đây là cách middle hoạt động: khi action.type là ADD_ARTICLE kiểm tra action.payload.title có chứa từ cấm. Nó sẽ dispatch một action có action type là FOUND_BAD_WORD, ngược lại, nó sẽ chạy đến middleware tiếp theo.

Giờ, hãy kết nối forbiddenWordsMiddleware với Redux store. Để làm được điều đó, chúng ra cần import middleware của mình.

Mở src/js/store/index.js, và sửa thành:

import { createStore, applyMiddleware, compose } from 'redux';

import rootReducer from '../reducers/index';

import { forbiddenWordsMiddleware } from '../middleware';


const storeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;


const store = createStore(

  rootReducer,

  storeEnhancers(applyMiddleware(forbiddenWordsMiddleware))

);


export default store;

Lưu lại và kiểm tra, khi thêm article có tên “money”, nó sẽ không xuất hiện trong list (có thể nhìn thấy rõ hơn trong Redux Devtools)

Asynchronous actions trong Redux

Dispatch action là đồng bộ. Không phải AJAX, không phải Promise.

Vậy khi muốn lấy dữ liệu bất đồng bộ từ API thì sao? Ở React, chúng ta sẽ gọi bên trong componentDidMount. Vậy còn Redux? Đâu là nơi tốt nhất để gọi hàm bất đồng bộ. Hãy thử suy nghĩ một chút.

Reducer? Không. Reducer nên được để clean. Reducer không phải nơi tốt nhất cho logic bất đồng bộ.

Action? Action trong Redux chỉ là một plain object. Còn action creator thì sao? Chính xác, action creator là một hàm và nó là nơi thích hợp nhất để gọi API.

Chúng ta sẽ lấy một API giả từ jsonplaceholder.typicode.com

Mở src/js/actions/index.js, và tạo một hàm có tên getData:

// ...

// our new action creator. Will it work?

export function getData() {

  return fetch("https://jsonplaceholder.typicode.com/posts")

    .then(response => response.json())

    .then(json => {

      return { type: "DATA_LOADED", payload: json };

    });

}

Tạo một component mới src/js/components/Post.js

import React, { Component } from "react";

import { connect } from "react-redux";

import { getData } from "../actions/index";


export class Post extends Component {


  componentDidMount() {

    // calling the new action creator

    this.props.getData();

  }


  render() {

    return null;

  }

}


export default connect(

  null,

  { getData }

)(Post);

Cuối cùng, cập nhật lại src/js/components/App.js:

import React from 'react';

import List from './js/components/List';

import Form from './js/components/Form';

import Post from './js/components/Post';


const App = () => (

  <>

    <div>

      <h2>Articles</h2>

      <List />

    </div>

    <div>

      <h2>Add a new article</h2>

      <Form />

    </div>

    <div>

      <h2>API posts</h2>

      <Post />

    </div>

  </>

);


export default App;

Lưu và chạy app sẽ thấy lỗi ở console: "Error: Actions must be plain objects. Use custom middleware for async actions". Chúng ta không thể fetch dữ liệu từ action creator trong Redux. Bây giờ phải làm sao?

Để mọi thứ có thể hoạt động, chúng ta cần một custom middleware. May mắn thay, có một thứ đã sẵn sàng cho chúng ta: redux-thunk.

Asynchronous actions trong Redux với redux-thunk

Fetch từ actions creator không hoạt động bởi vì Redux mong muốn nhận được một object, ở đây chúng ta cố gắng trả về một Promise.

Với redux-thunk (là một middleware của Redux) chúng ta có thể giải quyết vấn đề trả về một hàm từ action creator. Đây là cách chúng ta có thể gọi API, trì hoãn việc dispatch action và hơn thế nữa.

Đầu tiên, chúng ta cài middleware với:

npm i redux-thunk --save-dev

Thêm thunk trong src/js/store/index.js:

import { createStore, applyMiddleware, compose } from 'redux';

import rootReducer from '../reducers/index';

import { forbiddenWordsMiddleware } from '../middleware';

import thunk from 'redux-thunk';


const storeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;


const store = createStore(

  rootReducer,

  storeEnhancers(applyMiddleware(forbiddenWordsMiddleware, thunk))

);


export default store;

Sửa hàm getData để có thể dùng redux-thunk, mở src/js/actions/index.js:

export function getData() {

  return function(dispatch) {

    return fetch("https://jsonplaceholder.typicode.com/posts")

      .then(response => response.json())

      .then(json => {

        dispatch({ type: "DATA_LOADED", payload: json });

      });

  };

}

Lưu ý, phải dispatch bên trong then để dispatch action sau khi quá trình fetch hoàn tất.

Cập nhật reducer với action type mới, mở src/js/reducers/index.js:

import { ADD_ARTICLE, DATA_LOADED } from '../constants/action-types';


const initialState = {

  articles: [],

  remoteArticles: []

};


function rootReducer(state = initialState, action) {

  if (action.type === ADD_ARTICLE) {

    return {

      ...state,

      articles: state.articles.concat(action.payload)

    };

  }


  if (action.type === DATA_LOADED) {

    return {

      ...state,

      remoteArticles: state.remoteArticles.concat(action.payload)

    };

  }


  return state;

}


export default rootReducer;

Nhớ thêm DATA_LOADED trong src/js/constants/action-types.js:

export const ADD_ARTICLE = 'ADD_ARTICLE';

export const DATA_LOADED = 'DATA_LOADED';

Cuối cùng, chúng ta cập nhật lại Post component để hiển thị các post lấy được.

Mở src/js/components/Post.js:

import React, { Component } from "react";

import { connect } from "react-redux";

import { getData } from "../actions/index";


export class Post extends Component {

  constructor(props) {

    super(props);

  }


  componentDidMount() {

    this.props.getData();

  }


  render() {

    return (

      <ul>

        {this.props.articles.map(el => (

          <li key={el.id}>{el.title}</li>

        ))}

      </ul>

    );

  }

}


function mapStateToProps(state) {

  return {

    articles: state.remoteArticles.slice(0, 10)

  };

}


export default connect(

  mapStateToProps,

  { getData }

)(Post);

Lưu lại và chạy app, mọi thứ đã hoàn tất.

Tham khảo: https://www.valentinog.com/blog/ 

Cre: TVĐ

- Tech Zone -

Thư giãn chút nào!!!

Bài viết liên quan