ทำไมเราต้องการมิดเดิลแวร์สำหรับโฟลว์ async ใน Redux


686

ตามเอกสารที่"โดยตัวกลาง Redux เก็บสนับสนุนเฉพาะการไหลของข้อมูลซิงโคร" ฉันไม่เข้าใจว่าทำไมถึงเป็นเช่นนี้ ทำไมส่วนประกอบคอนเทนเนอร์ไม่สามารถเรียกใช้ async API จากนั้นจึงdispatchดำเนินการ

ตัวอย่างเช่นลองจินตนาการถึง UI ที่เรียบง่าย: ฟิลด์และปุ่ม เมื่อผู้ใช้กดปุ่มฟิลด์จะถูกเติมด้วยข้อมูลจากเซิร์ฟเวอร์ระยะไกล

เขตข้อมูลและปุ่ม

import * as React from 'react';
import * as Redux from 'redux';
import { Provider, connect } from 'react-redux';

const ActionTypes = {
    STARTED_UPDATING: 'STARTED_UPDATING',
    UPDATED: 'UPDATED'
};

class AsyncApi {
    static getFieldValue() {
        const promise = new Promise((resolve) => {
            setTimeout(() => {
                resolve(Math.floor(Math.random() * 100));
            }, 1000);
        });
        return promise;
    }
}

class App extends React.Component {
    render() {
        return (
            <div>
                <input value={this.props.field}/>
                <button disabled={this.props.isWaiting} onClick={this.props.update}>Fetch</button>
                {this.props.isWaiting && <div>Waiting...</div>}
            </div>
        );
    }
}
App.propTypes = {
    dispatch: React.PropTypes.func,
    field: React.PropTypes.any,
    isWaiting: React.PropTypes.bool
};

const reducer = (state = { field: 'No data', isWaiting: false }, action) => {
    switch (action.type) {
        case ActionTypes.STARTED_UPDATING:
            return { ...state, isWaiting: true };
        case ActionTypes.UPDATED:
            return { ...state, isWaiting: false, field: action.payload };
        default:
            return state;
    }
};
const store = Redux.createStore(reducer);
const ConnectedApp = connect(
    (state) => {
        return { ...state };
    },
    (dispatch) => {
        return {
            update: () => {
                dispatch({
                    type: ActionTypes.STARTED_UPDATING
                });
                AsyncApi.getFieldValue()
                    .then(result => dispatch({
                        type: ActionTypes.UPDATED,
                        payload: result
                    }));
            }
        };
    })(App);
export default class extends React.Component {
    render() {
        return <Provider store={store}><ConnectedApp/></Provider>;
    }
}

เมื่อมีการแสดงองค์ประกอบที่ส่งออกฉันสามารถคลิกปุ่มและอินพุตได้รับการปรับปรุงอย่างถูกต้อง

หมายเหตุupdateฟังก์ชั่นการconnectโทร มันยื้อการกระทำที่บอกแอพว่ากำลังอัปเดตแล้วทำการโทรแบบ async หลังจากการโทรเสร็จสิ้นค่าที่ระบุจะถูกจัดส่งเป็นเพย์โหลดของการดำเนินการอื่น

เกิดอะไรขึ้นกับวิธีการนี้ ทำไมฉันถึงต้องการใช้ Redux Thunk หรือ Redux Promise ตามเอกสารแนะนำ?

แก้ไข:ฉันค้นหา Redux repo เพื่อหาเบาะแสและพบว่า Action Builders จำเป็นต้องใช้ในการทำงานที่บริสุทธิ์ในอดีต ตัวอย่างเช่นต่อไปนี้เป็นผู้ใช้ที่พยายามให้คำอธิบายที่ดีกว่าสำหรับการไหลของข้อมูล async:

ผู้สร้างแอคชั่นเองยังคงเป็นฟังก์ชั่นแท้ๆ แต่ฟังก์ชั่น thunk ที่ส่งคืนไม่จำเป็นต้องเป็นและสามารถเรียกใช้ async ของเราได้

ผู้สร้าง Action ไม่จำเป็นต้องบริสุทธิ์อีกต่อไป ในอดีตที่ผ่านมามิดเดิลแวร์ / สัญญาจำเป็นต้องใช้อย่างแน่นอน แต่ดูเหมือนว่านี่จะไม่เป็นเช่นนั้นอีกต่อไป?


53
ผู้สร้าง Action ไม่จำเป็นต้องเป็นฟังก์ชั่นที่บริสุทธิ์ มันเป็นความผิดพลาดในเอกสารไม่ใช่การตัดสินใจที่เปลี่ยนแปลง
Dan Abramov

1
@DanAbramov สำหรับการทดสอบมันอาจจะเป็นวิธีปฏิบัติที่ดีอย่างไรก็ตาม Redux-saga อนุญาตสิ่งนี้: stackoverflow.com/a/34623840/82609
Sebastien Lorber

คำตอบ:


702

เกิดอะไรขึ้นกับวิธีการนี้ ทำไมฉันถึงต้องการใช้ Redux Thunk หรือ Redux Promise ตามเอกสารแนะนำ?

ไม่มีอะไรผิดปกติกับวิธีนี้ มันไม่สะดวกในแอปพลิเคชันขนาดใหญ่เพราะคุณจะมีส่วนประกอบต่าง ๆ ที่ทำงานเหมือนกันคุณอาจต้องการ debounce การกระทำบางอย่างหรือรักษาสถานะท้องถิ่นบางอย่างเช่นการเพิ่ม ID อัตโนมัติใกล้กับผู้สร้างแอคชั่น ฯลฯ ดังนั้นจึงง่ายกว่า มุมมองการบำรุงรักษาเพื่อแยกผู้สร้างแอ็คชั่นออกเป็นฟังก์ชันแยกต่างหาก

คุณสามารถอ่านคำตอบของฉันเกี่ยวกับ“ วิธีส่งการกระทำของ Redux ด้วยการหมดเวลา”สำหรับคำแนะนำแบบละเอียดมากขึ้น

มิดเดิลแวร์เช่น Redux Thunk หรือ Redux Promise เพียงแค่ให้ "ซินแท็กซ์น้ำตาล" สำหรับการจัดส่ง thunks หรือสัญญา แต่คุณไม่ต้องใช้

ดังนั้นหากไม่มีมิดเดิ้ลใด ๆ ผู้สร้างแอคชั่นของคุณอาจมีลักษณะเช่นนั้น

// action creator
function loadData(dispatch, userId) { // needs to dispatch, so it is first argument
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
    );
}

// component
componentWillMount() {
  loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch
}

แต่ด้วย Thunk Middleware คุณสามารถเขียนดังนี้:

// action creator
function loadData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`) // Redux Thunk handles these
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
    );
}

// component
componentWillMount() {
  this.props.dispatch(loadData(this.props.userId)); // dispatch like you usually do
}

ดังนั้นจึงไม่มีความแตกต่างอย่างมาก สิ่งหนึ่งที่ฉันชอบเกี่ยวกับวิธีการหลังคือส่วนประกอบไม่สนใจว่าผู้สร้างแอคชั่นนั้นเป็นแบบอะซิงโครนัส มันแค่เรียกdispatchตามปกติมันยังสามารถใช้mapDispatchToPropsผูกผู้สร้างแอ็คชั่นดังกล่าวด้วยซินแทกซ์สั้น ๆ ส่วนประกอบต่าง ๆ ไม่ทราบว่าผู้สร้างแอคชั่นมีการใช้งานอย่างไรและคุณสามารถสลับระหว่างแนวทาง async ที่แตกต่างกันได้ ) โดยไม่ต้องเปลี่ยนส่วนประกอบ ในทางกลับกันด้วยวิธีการเดิมที่ชัดเจนส่วนประกอบของคุณรู้ว่าการโทรที่เฉพาะเจาะจงนั้นเป็นแบบอะซิงโครนัสและจำเป็นdispatchต้องผ่านการประชุมบางอย่าง (เช่นเป็นพารามิเตอร์การซิงค์)

ลองคิดดูว่าจะเปลี่ยนรหัสนี้อย่างไร สมมติว่าเราต้องการมีฟังก์ชั่นการโหลดข้อมูลที่สองและรวมเข้าด้วยกันในผู้สร้างแอ็คชั่นเดียว

ด้วยวิธีแรกที่เราต้องคำนึงถึงผู้สร้างแอ็คชั่นที่เรากำลังเรียกร้อง:

// action creators
function loadSomeData(dispatch, userId) {
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
    );
}
function loadOtherData(dispatch, userId) {
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
    );
}
function loadAllData(dispatch, userId) {
  return Promise.all(
    loadSomeData(dispatch, userId), // pass dispatch first: it's async
    loadOtherData(dispatch, userId) // pass dispatch first: it's async
  );
}


// component
componentWillMount() {
  loadAllData(this.props.dispatch, this.props.userId); // pass dispatch first
}

ด้วย Redux Thunk action creators สามารถdispatchเป็นผลมาจากผู้สร้างแอ็คชั่นอื่น ๆ และไม่คิดว่าจะเป็นแบบซิงโครนัสหรือแบบอะซิงโครนัส:

// action creators
function loadSomeData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
    );
}
function loadOtherData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
    );
}
function loadAllData(userId) {
  return dispatch => Promise.all(
    dispatch(loadSomeData(userId)), // just dispatch normally!
    dispatch(loadOtherData(userId)) // just dispatch normally!
  );
}


// component
componentWillMount() {
  this.props.dispatch(loadAllData(this.props.userId)); // just dispatch normally!
}

ด้วยวิธีการนี้หากคุณต้องการให้ผู้สร้างแอคชั่นของคุณดูสถานะ Redux ปัจจุบันคุณสามารถใช้getStateอาร์กิวเมนต์ตัวที่สองที่ส่งผ่านไปยัง thunks โดยไม่ต้องแก้ไขรหัสโทรศัพท์เลย:

function loadSomeData(userId) {
  // Thanks to Redux Thunk I can use getState() here without changing callers
  return (dispatch, getState) => {
    if (getState().data[userId].isLoaded) {
      return Promise.resolve();
    }

    fetch(`http://data.com/${userId}`)
      .then(res => res.json())
      .then(
        data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
        err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
      );
  }
}

หากคุณต้องการเปลี่ยนให้เป็นแบบซิงโครนัสคุณสามารถทำได้โดยไม่ต้องเปลี่ยนรหัสการโทรใด ๆ :

// I can change it to be a regular action creator without touching callers
function loadSomeData(userId) {
  return {
    type: 'LOAD_SOME_DATA_SUCCESS',
    data: localStorage.getItem('my-data')
  }
}

ดังนั้นข้อดีของการใช้มิดเดิลแวร์เช่น Redux Thunk หรือ Redux Promise คือส่วนประกอบไม่ทราบว่าผู้สร้างแอ็คชั่นดำเนินการอย่างไรและพวกเขาสนใจสถานะ Redux ไม่ว่าจะซิงโครนัสหรืออะซิงโครนัสหรือไม่ . ข้อเสียคือการอ้อมไปเล็กน้อย แต่เราเชื่อว่ามันคุ้มค่าสำหรับการใช้งานจริง

ในที่สุด Redux Thunk และเพื่อน ๆ เป็นเพียงวิธีหนึ่งที่เป็นไปได้ในการร้องขอแบบไม่ซิงค์ในแอพ Redux อีกวิธีที่น่าสนใจคือRedux Sagaซึ่งช่วยให้คุณกำหนด daemons ที่ใช้เวลานาน (“ sagas”) ที่ดำเนินการตามที่มาและแปลงหรือดำเนินการตามคำขอก่อนที่จะส่งออกการกระทำ นี่เป็นการย้ายตรรกะจากผู้สร้างแอ็คชั่นมาเป็น sagas คุณอาจต้องการตรวจสอบและหลังจากนั้นเลือกสิ่งที่เหมาะกับคุณมากที่สุด

ฉันค้นหา Redux repo เพื่อหาเบาะแสและพบว่า Action Creators จำเป็นต้องใช้เพื่อการทำงานที่บริสุทธิ์ในอดีต

สิ่งนี้ไม่ถูกต้อง เอกสารกล่าวสิ่งนี้ แต่เอกสารผิด
ผู้สร้าง Action ไม่จำเป็นต้องเป็นฟังก์ชั่นที่บริสุทธิ์
เราแก้ไขเอกสารเพื่อสะท้อนสิ่งนั้น


57
อาจเป็นวิธีสั้น ๆ ในการบอกความคิดของแดนว่า: มิดเดิลแวร์เป็นวิธีการรวมศูนย์ด้วยวิธีนี้ช่วยให้คุณทำให้ส่วนประกอบของคุณง่ายขึ้นและเป็นแบบทั่วไปและควบคุมการไหลของข้อมูลในที่เดียว หากคุณดูแลแอพขนาดใหญ่คุณจะสนุกไปกับมัน =)
Sergey Lapin

3
@ asdfasdfads ฉันไม่เห็นว่าทำไมมันไม่ทำงาน มันจะทำงานเหมือนเดิมทุกประการ ใส่alertหลังจากdispatch()การกระทำ
Dan Abramov

9
บรรทัดสุดท้ายในตัวอย่างโค้ดแรกของคุณ: loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch. ทำไมฉันต้องผ่านการจัดส่ง ถ้าโดยการประชุมมีที่เคยมีเพียงร้านเดียวทั่วโลกทำไมฉันจึงไม่เพียงแค่ทำอ้างอิงโดยตรงและทำ store.dispatchทุกครั้งที่ผมต้องเช่นในloadData?
Søren Debois

10
@ SørenDeboisหากแอปของคุณเป็นฝั่งไคลเอ็นต์เท่านั้นที่จะใช้งานได้ หากมีการแสดงผลบนเซิร์ฟเวอร์คุณจะต้องมีstoreอินสแตนซ์ที่แตกต่างกันสำหรับทุกคำขอเพื่อให้คุณไม่สามารถกำหนดไว้ล่วงหน้า
Dan Abramov

3
เพียงแค่ต้องการชี้ให้เห็นว่าคำตอบนี้มี 139 บรรทัดซึ่งมากกว่าโค้ดต้นฉบับของ redux-thunk 9.92 เท่าซึ่งประกอบด้วย 14 บรรทัด: github.com/gaearon/redux-thunk/blob/master/src/index.js
Guy

447

คุณทำไม่ได้

แต่ ... คุณควรใช้ redux-saga :)

คำตอบของ Dan Abramov นั้นถูกต้องredux-thunkแต่ฉันจะพูดเพิ่มเติมเกี่ยวกับredux-sagaที่ค่อนข้างคล้ายกัน แต่มีประสิทธิภาพมากกว่า

การประกาศ VS แบบบังคับ

  • DOM : jQuery มีความจำเป็น / React คือการประกาศ
  • Monads : IO มีความจำเป็น / ฟรีเป็นสิ่งที่ประกาศได้
  • ผลกระทบ Redux : redux-thunkมีความจำเป็น / redux-sagaมีการประกาศ

เมื่อคุณมีมืออันธพาลเช่นไอโอ monad หรือสัญญาคุณไม่สามารถรู้ได้อย่างง่ายดายว่าจะทำอย่างไรเมื่อคุณดำเนินการ วิธีเดียวที่จะทดสอบอันธพาลคือดำเนินการและเยาะเย้ยดิสแพตเชอร์ (หรือโลกภายนอกทั้งหมดถ้ามันโต้ตอบกับสิ่งอื่น ๆ อีกมากมาย ... )

หากคุณใช้ mocks แสดงว่าคุณไม่ได้เขียนโปรแกรมใช้งานได้

หากมองผ่านเลนส์ของผลข้างเคียง mocks คือธงที่รหัสของคุณไม่บริสุทธิ์และในสายตาของโปรแกรมเมอร์ที่ใช้งานได้พิสูจน์ว่ามีบางอย่างผิดปกติ แทนที่จะดาวน์โหลดห้องสมุดเพื่อช่วยให้เราตรวจสอบภูเขาน้ำแข็งไม่เป็นอันตรายเราควรล่องเรือรอบ ๆ คนที่แต่งตัวประหลาด TDD / Java ไม่ยอมใครง่ายๆเคยถามฉันว่าคุณล้อเลียนใน Clojure อย่างไร คำตอบคือเรามักจะไม่ทำ เรามักจะเห็นว่ามันเป็นสัญญาณที่เราจำเป็นต้อง refactor รหัสของเรา

แหล่ง

sagas (ตามที่พวกเขานำมาใช้redux-saga) เป็นสิ่งที่ประกาศได้และเช่นเดียวกับส่วนประกอบของ monad หรือ React พวกเขาทดสอบได้ง่ายกว่าโดยไม่มีการจำลองใด ๆ

ดูบทความนี้ :

ใน FP ที่ทันสมัยเราไม่ควรเขียนโปรแกรม - เราควรเขียนคำอธิบายของโปรแกรมซึ่งเราสามารถวิปัสสนา, แปลงร่างและตีความตามความประสงค์

(ที่จริงแล้ว Redux-saga เป็นเหมือนลูกผสม: การไหลเป็นสิ่งจำเป็น แต่ผลที่ได้คือการประกาศ)

ความสับสน: การกระทำ / เหตุการณ์ / คำสั่ง ...

มีความสับสนมากมายในโลกหน้าว่าแนวคิดแบ็คเอนด์เช่น CQRS / EventSourcing และ Flux / Redux อาจเกี่ยวข้องกันส่วนใหญ่เป็นเพราะใน Flux เราใช้คำว่า "การกระทำ" ซึ่งบางครั้งอาจเป็นตัวแทนของรหัสบังคับ ( LOAD_USER) และเหตุการณ์ ( USER_LOADED) ฉันเชื่อว่าเช่นเดียวกับการจัดหากิจกรรมคุณควรจัดส่งกิจกรรมเท่านั้น

การใช้ sagas ในทางปฏิบัติ

ลองนึกภาพแอปที่มีลิงก์ไปยังโปรไฟล์ผู้ใช้ วิธีการใช้สำนวนนี้ในการจัดการกับมิดเดิลแวร์แต่ละอันคือ:

redux-thunk

<div onClick={e => dispatch(actions.loadUserProfile(123)}>Robert</div>

function loadUserProfile(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'USER_PROFILE_LOADED', data }),
      err => dispatch({ type: 'USER_PROFILE_LOAD_FAILED', err })
    );
}

redux-saga

<div onClick={e => dispatch({ type: 'USER_NAME_CLICKED', payload: 123 })}>Robert</div>


function* loadUserProfileOnNameClick() {
  yield* takeLatest("USER_NAME_CLICKED", fetchUser);
}

function* fetchUser(action) {
  try {
    const userProfile = yield fetch(`http://data.com/${action.payload.userId }`)
    yield put({ type: 'USER_PROFILE_LOADED', userProfile })
  } 
  catch(err) {
    yield put({ type: 'USER_PROFILE_LOAD_FAILED', err })
  }
}

เทพนิยายนี้แปลเป็น:

ทุกครั้งที่มีการคลิกชื่อผู้ใช้ดึงโปรไฟล์ผู้ใช้แล้วส่งเหตุการณ์ด้วยโปรไฟล์ที่โหลด

redux-sagaที่คุณสามารถดูมีประโยชน์บางส่วนของ

การtakeLatestอนุญาตให้ใช้เพื่อแสดงว่าคุณสนใจที่จะรับข้อมูลชื่อผู้ใช้ล่าสุดที่คลิกเท่านั้น (จัดการปัญหาที่เกิดขึ้นพร้อมกันในกรณีที่ผู้ใช้คลิกเร็วมากในชื่อผู้ใช้จำนวนมาก) สิ่งของประเภทนี้มีความแข็งมาก คุณสามารถใช้takeEveryถ้าคุณไม่ต้องการพฤติกรรมนี้

คุณทำให้ผู้สร้างแอ็คชั่นบริสุทธิ์ โปรดทราบว่ามันยังมีประโยชน์ในการรักษา actionCreators (ใน sagas putและส่วนประกอบdispatch) เนื่องจากมันอาจช่วยให้คุณเพิ่มการตรวจสอบการกระทำ (ยืนยัน / ไหล / typescript) ในอนาคต

รหัสของคุณสามารถทดสอบได้มากขึ้นเนื่องจากเอฟเฟกต์เป็นสิ่งที่เปิดเผยได้

คุณไม่จำเป็นอีกต่อไปที่ทริกเกอร์ actions.loadUser()RPC-เช่นสายเช่น UI ของคุณเพียงต้องการส่งสิ่งที่เกิดขึ้น เราเพียง แต่จัดกิจกรรม (ในช่วงเวลาที่ผ่านมา!) และไม่ใช่การกระทำอีกต่อไป ซึ่งหมายความว่าคุณสามารถสร้าง"เป็ด"หรือบริบทที่ถูกผูกมัดแบบแยกส่วนและเทพนิยายสามารถทำหน้าที่เป็นจุดเชื่อมต่อระหว่างส่วนประกอบแบบแยกส่วนเหล่านี้

ซึ่งหมายความว่ามุมมองของคุณง่ายต่อการจัดการเพราะพวกเขาไม่ต้องการที่จะมีเลเยอร์การแปลนั้นระหว่างสิ่งที่เกิดขึ้นกับสิ่งที่ควรจะเกิดขึ้น

ตัวอย่างเช่นลองนึกภาพมุมมองการเลื่อนที่ไม่มีที่สิ้นสุด CONTAINER_SCROLLEDสามารถนำไปสู่NEXT_PAGE_LOADEDแต่เป็นความรับผิดชอบจริง ๆ ของคอนเทนเนอร์ที่เลื่อนได้เพื่อตัดสินใจว่าเราควรโหลดหน้าอื่นหรือไม่ จากนั้นเขาต้องระวังสิ่งที่ซับซ้อนมากขึ้นเช่นโหลดหน้าสุดท้ายสำเร็จหรือไม่หรือมีหน้าเว็บที่พยายามโหลดหรือไม่มีรายการเหลือให้โหลดหรือไม่ ฉันไม่คิดอย่างนั้น: เพื่อให้สามารถนำกลับมาใช้ใหม่ได้สูงสุดคอนเทนเนอร์แบบเลื่อนได้ควรอธิบายว่ามันถูกเลื่อน การโหลดหน้าเว็บเป็น "ผลกระทบทางธุรกิจ" ของการเลื่อนนั้น

บางคนอาจแย้งว่าเครื่องกำเนิดไฟฟ้าสามารถซ่อนสถานะนอกร้าน redux ด้วยตัวแปรท้องถิ่นได้ แต่ถ้าคุณเริ่มดัดแปลงสิ่งที่ซับซ้อนภายใน thunks โดยเริ่มตัวนับ ฯลฯ คุณจะมีปัญหาเดียวกันอยู่แล้ว และมีselectผลกระทบที่ตอนนี้อนุญาตให้รับสถานะจากร้านค้า Redux ของคุณ

Sagas สามารถเดินทางข้ามเวลาและยังช่วยให้การบันทึกการไหลที่ซับซ้อนและเครื่องมือ dev ที่กำลังทำงานอยู่ นี่คือบันทึกการไหลของ async แบบง่าย ๆ ที่มีการใช้งานแล้ว:

saga flow logging

decoupling

Sagas ไม่เพียง แต่แทนที่ redux thunks พวกเขามาจากแบ็กเอนด์ / กระจายระบบ / จัดหาเหตุการณ์

มันเป็นความเข้าใจผิดที่พบบ่อยมากที่ sagas เป็นเพียงที่นี่เพื่อแทนที่ thux ของคุณด้วยการทดสอบที่ดีขึ้น จริงๆแล้วนี่เป็นเพียงรายละเอียดการดำเนินการของ redux-saga การใช้เอฟเฟกต์การประกาศดีกว่าชุดทดสอบที่ใช้ได้ แต่รูปแบบการผจญภัยสามารถนำไปใช้กับโค้ดที่จำเป็นหรือที่เปิดเผยได้

ในตอนแรกซากะเป็นซอฟต์แวร์ที่อนุญาตให้ประสานงานธุรกรรมที่ดำเนินการมายาวนาน (ความสอดคล้องในที่สุด) และการทำธุรกรรมในบริบทที่แตกต่างกัน

เพื่อทำให้โลกนี้ง่ายขึ้นให้จินตนาการว่ามี widget1 และ widget2 เมื่อคลิกปุ่มบน widget1 นั้นจะมีผลกับ widget2 แทนที่จะเชื่อมต่อ 2 วิดเจ็ตเข้าด้วยกัน (เช่น widget1 ส่งการกระทำที่กำหนดเป้าหมาย widget2), widget1 จะส่งเฉพาะปุ่มที่คลิก จากนั้นเทพนิยายจะฟังปุ่มนี้คลิกจากนั้นอัปเดต widget2 โดยแยกเหตุการณ์ใหม่ที่ widget2 รับรู้

สิ่งนี้เป็นการเพิ่มระดับของการอ้อมที่ไม่จำเป็นสำหรับแอพที่ง่าย แต่ทำให้ง่ายต่อการปรับขนาดแอพพลิเคชั่นที่ซับซ้อน ตอนนี้คุณสามารถเผยแพร่ widget1 และ widget2 ไปยังที่เก็บ npm ที่แตกต่างกันเพื่อให้พวกเขาไม่ต้องรู้เกี่ยวกับกันและกันโดยไม่ต้องให้พวกเขาแชร์การลงทะเบียนระดับโลกของการกระทำ วิดเจ็ต 2 ตอนนี้ถูกผูกไว้กับบริบทที่สามารถอยู่แยกกันได้ พวกเขาไม่ต้องการให้แต่ละคนมีความสอดคล้องกันและสามารถนำกลับมาใช้ในแอปอื่น ๆ ได้เช่นกัน เทพนิยายเป็นจุดเชื่อมต่อระหว่างสองวิดเจ็ตที่ประสานงานพวกมันอย่างมีความหมายสำหรับธุรกิจของคุณ

บทความดีๆเกี่ยวกับวิธีการจัดโครงสร้างแอป Redux ของคุณซึ่งคุณสามารถใช้ Redux-saga ได้ด้วยเหตุผล decoupling:

usecase ที่เป็นรูปธรรม: ระบบแจ้งเตือน

ฉันต้องการให้ส่วนประกอบของฉันสามารถเรียกการแสดงการแจ้งเตือนในแอพได้ แต่ฉันไม่ต้องการให้ส่วนประกอบของฉันเชื่อมโยงกับระบบการแจ้งเตือนที่มีกฎเกณฑ์ทางธุรกิจของตัวเองสูง (การแจ้งเตือนสูงสุด 3 รายการปรากฏขึ้นในเวลาเดียวกันการจัดคิวการแจ้งเตือนเวลาแสดงผล 4 วินาทีเป็นต้น ... )

ฉันไม่ต้องการให้คอมโพเนนต์ JSX ตัดสินใจว่าการแจ้งเตือนจะแสดง / ซ่อนเมื่อใด ฉันแค่ให้ความสามารถในการร้องขอการแจ้งเตือนและปล่อยให้กฎที่ซับซ้อนภายในเทพนิยาย สิ่งของประเภทนี้ค่อนข้างยากที่จะนำไปใช้กับอุโมงค์หรือสัญญา

การแจ้งเตือน

ฉันอธิบายแล้ว ที่นี่ว่าสิ่งนี้สามารถทำได้กับซากะ

ทำไมถึงเรียกว่าซากะ?

เทพนิยายเป็นคำที่มาจากโลกแบ็กเอนด์ ตอนแรกฉันแนะนำ Yassine (ผู้แต่ง Redux-saga) กับคำนั้นในการสนทนาที่ยาวนานการอภิปรายยาว

เริ่มแรกคำนี้ถูกนำเสนอด้วยกระดาษรูปแบบเทพนิยายที่ควรจะใช้ในการจัดการความสอดคล้องในที่สุดในการทำธุรกรรมกระจาย แต่การใช้งานได้รับการขยายไปยังคำจำกัดความที่กว้างขึ้นโดยนักพัฒนาแบ็กเอนด์เพื่อให้ตอนนี้ก็ครอบคลุม pattern (รูปแบบ saga ดั้งเดิมเป็นรูปแบบเฉพาะของตัวจัดการกระบวนการ)

วันนี้คำว่า "เทพนิยาย" กำลังสับสนเพราะมันสามารถอธิบายสิ่งต่าง ๆ ได้ 2 อย่าง เนื่องจากมันถูกใช้ใน redux-saga มันไม่ได้อธิบายถึงวิธีการจัดการธุรกรรมแบบกระจาย แต่เป็นวิธีการประสานการทำงานในแอปของคุณ redux-sagaอาจถูกเรียกredux-process-managerอีกอย่างว่า

ดูสิ่งนี้ด้วย:

ทางเลือก

หากคุณไม่ชอบความคิดในการใช้เครื่องกำเนิดไฟฟ้า แต่คุณสนใจรูปแบบการผจญภัยและคุณสมบัติการแยกชิ้นส่วนคุณสามารถบรรลุผลเช่นเดียวกันกับredux-observableซึ่งใช้ชื่อepicเพื่ออธิบายรูปแบบเดียวกัน แต่ด้วย RxJS หากคุณคุ้นเคยกับ Rx แล้วคุณจะรู้สึกเหมือนอยู่บ้าน

const loadUserProfileOnNameClickEpic = action$ =>
  action$.ofType('USER_NAME_CLICKED')
    .switchMap(action =>
      Observable.ajax(`http://data.com/${action.payload.userId}`)
        .map(userProfile => ({
          type: 'USER_PROFILE_LOADED',
          userProfile
        }))
        .catch(err => Observable.of({
          type: 'USER_PROFILE_LOAD_FAILED',
          err
        }))
    );

ทรัพยากรที่มีประโยชน์ redux-saga

2017 ให้คำแนะนำ

  • อย่าใช้ Redux-saga มากเกินไปเพียงเพื่อประโยชน์ในการใช้งานเท่านั้น การเรียกใช้ API ที่ทดสอบได้เท่านั้นไม่คุ้มค่า
  • อย่าลบ thunks ออกจากโครงการของคุณสำหรับกรณีที่ง่ายที่สุด
  • อย่าลังเลที่จะส่ง thunks yield put(someActionThunk)ถ้ามันสมเหตุสมผล

หากคุณกลัวที่จะใช้ Redux-saga (หรือ Redux-observable) แต่ต้องการรูปแบบ decoupling ให้ตรวจสอบredux-dispatch-สมัครสมาชิก : มันอนุญาตให้ฟัง dispatches และทริกเกอร์ dispatches ใหม่ในผู้ฟัง

const unsubscribe = store.addDispatchListener(action => {
  if (action.type === 'ping') {
    store.dispatch({ type: 'pong' });
  }
});

64
นี่จะดีขึ้นทุกครั้งที่ฉันกลับมา ลองเปลี่ยนเป็นโพสต์บล็อก :)
RainerAtSpirit

4
ขอบคุณสำหรับการเขียนที่ดี อย่างไรก็ตามฉันไม่เห็นด้วยกับบางประเด็น LOAD_USER มีความจำเป็นอย่างไร สำหรับฉันแล้วมันไม่ได้เป็นเพียงแค่การประกาศเท่านั้น แต่ยังให้โค้ดที่อ่านง่าย ชอบเช่น "เมื่อฉันกดปุ่มนี้ฉันต้องการ ADD_ITEM" ฉันสามารถดูรหัสและเข้าใจว่าเกิดอะไรขึ้น หากมันถูกเรียกว่าบางสิ่งบางอย่างจากผลกระทบของ "BUTTON_CLICK" ฉันต้องค้นหามัน
swelet

4
คำตอบที่ดี มีทางเลือกอื่นในขณะนี้: github.com/blesh/redux-observable
swennemen

4
@swelet ขออภัยสำหรับ anwer ปลาย เมื่อคุณส่งADD_ITEMมันมีความจำเป็นเพราะคุณส่งการกระทำที่มุ่งหวังที่จะมีผลกระทบต่อร้านค้าของคุณ: คุณคาดหวังว่าการกระทำที่จะทำอะไรบางอย่าง การประกาศให้เป็นไปตามปรัชญาของการจัดหากิจกรรม: คุณไม่ได้ส่งการกระทำเพื่อกระตุ้นการเปลี่ยนแปลงแอปพลิเคชันของคุณ แต่คุณส่งเหตุการณ์ที่ผ่านมาเพื่ออธิบายสิ่งที่เกิดขึ้นในแอปพลิเคชันของคุณ การส่งเหตุการณ์ควรเพียงพอที่จะพิจารณาว่าสถานะของแอปพลิเคชันมีการเปลี่ยนแปลง ความจริงที่ว่ามีร้านค้า Redux ที่ตอบสนองต่อเหตุการณ์นั้นเป็นรายละเอียดการติดตั้งเสริม
Sebastien Lorber

3
ฉันไม่ชอบคำตอบนี้เพราะมันเบี่ยงเบนไปจากคำถามจริงเพื่อทำการตลาดห้องสมุดของใครบางคน คำตอบนี้ให้การเปรียบเทียบของสองไลบรารีซึ่งไม่ใช่เจตนาของคำถาม คำถามที่แท้จริงคือการถามว่าจะใช้มิดเดิลแวร์เลยหรือไม่ซึ่งอธิบายโดยคำตอบที่ได้รับการยอมรับ
Abhinav Manchanda

31

คำตอบสั้น ๆ : ดูเหมือนว่าเป็นวิธีการที่สมเหตุสมผลโดยสิ้นเชิงกับปัญหาอะซิงโครนัสกับฉัน ด้วยข้อแม้คู่

ฉันมีความคิดที่คล้ายกันมากเมื่อทำงานในโครงการใหม่ที่เราเพิ่งเริ่มงานของฉัน ฉันเป็นแฟนตัวยงของระบบอันหรูหราของ Vanilla Redux สำหรับการอัปเดตร้านค้าและส่วนประกอบการแสดงผลซ้ำในลักษณะที่อยู่เหนือความกล้าหาญของโครงสร้างส่วนประกอบ React มันดูแปลกสำหรับฉันที่จะเข้าไปในdispatchกลไกอันสง่างามเพื่อจัดการกับอะซิงโครนัส

ฉันสิ้นสุดขึ้นไปด้วยวิธีการที่คล้ายกันจริงๆกับสิ่งที่คุณต้องมีในห้องสมุดผมเอาเรื่องของโครงการของเราซึ่งเราเรียกว่าตอบสนอง-Redux คอนโทรลเลอร์

ฉันลงเอยด้วยการไม่ไปตามแนวทางที่ถูกต้องที่คุณมีด้วยเหตุผลสองประการ:

  1. วิธีที่คุณเขียนมันฟังก์ชั่นการจ่ายงานเหล่านั้นไม่มีสิทธิ์เข้าถึงร้านค้า คุณสามารถแก้ไขได้โดยให้ส่วนประกอบ UI ของคุณส่งผ่านข้อมูลทั้งหมดที่ฟังก์ชั่นการจัดส่งต้องการ แต่ฉันก็เถียงว่าสิ่งนี้เป็นส่วนหนึ่งของ UI เหล่านั้นกับตรรกะการจัดส่งโดยไม่จำเป็น และเป็นปัญหามากขึ้นไม่มีวิธีที่ชัดเจนสำหรับฟังก์ชั่นการจัดส่งเพื่อเข้าถึงสถานะที่ได้รับการปรับปรุงในการดำเนินการต่อเนื่องของ async
  2. ฟังก์ชั่นการจัดส่งมีการเข้าถึงdispatchตัวเองผ่านขอบเขตศัพท์ สิ่งนี้จะ จำกัด ตัวเลือกสำหรับการ refactoring เมื่อconnectคำสั่งนั้นหมดไป - และมันก็ดูไม่น่าไว้วางใจด้วยupdateวิธีการเพียงวิธีเดียว ดังนั้นคุณต้องมีระบบบางอย่างเพื่อให้คุณสามารถเขียนฟังก์ชั่นโปรแกรมเลือกจ่ายงานเหล่านั้นถ้าคุณแยกมันออกเป็นโมดูลแยก

เมื่อนำมารวมกันคุณจะต้องเพิ่มระบบบางอย่างเพื่อให้อนุญาตdispatchและร้านค้าจะถูกฉีดเข้าไปในฟังก์ชั่นการจัดส่งของคุณพร้อมกับพารามิเตอร์ของเหตุการณ์ ฉันรู้วิธีที่สมเหตุสมผลสามวิธีในการฉีดพึ่งพานี้:

  • Redux-thunkทำสิ่งนี้ในลักษณะที่ใช้งานได้โดยส่งผ่านเข้าไปใน thunks ของคุณ (ทำให้พวกมันไม่ใช่ thunks เลยตามคำจำกัดความของโดม) ฉันไม่ได้ทำงานกับdispatchวิธีมิดเดิลแวร์อื่น ๆแต่ฉันคิดว่ามันเหมือนกัน
  • react-redux-controller ทำสิ่งนี้กับ coroutine โบนัสยังช่วยให้คุณเข้าถึง "selectors" ซึ่งเป็นฟังก์ชั่นที่คุณอาจส่งผ่านเป็นอาร์กิวเมนต์แรกไปconnectแทนที่จะต้องทำงานโดยตรงกับร้านค้าปกติ
  • คุณยังสามารถทำมันด้วยวิธีการเชิงวัตถุโดยการแทรกเข้าไปในthisบริบทผ่านกลไกต่าง ๆ ที่เป็นไปได้

ปรับปรุง

มันเกิดขึ้นกับฉันว่าส่วนหนึ่งของปริศนานี้เป็นข้อ จำกัด ของปฏิกิริยา - รีดักซ์ อาร์กิวเมนต์แรกที่connectได้รับสแนปชอตของรัฐ แต่ไม่ใช่การจัดส่ง อาร์กิวเมนต์ที่สองได้รับการจัดส่ง แต่ไม่ใช่สถานะ อาร์กิวเมนต์ไม่ได้รับ thunk ที่ปิดเหนือสถานะปัจจุบันสำหรับความสามารถในการดูสถานะที่ปรับปรุงในเวลาที่มีการต่อเนื่อง / ติดต่อกลับ


22

เป้าหมาย Abramov - และทุกคนนึกคิด - เป็นเพียงเพื่อความซับซ้อนแค็ปซูล (และโทร async) ในสถานที่ที่มันเหมาะสมที่สุด

ที่ไหนดีที่สุดในการทำเช่นนั้นใน Redux dataflow มาตรฐาน เกี่ยวกับ:

  • reducers ? ไม่มีทาง. ควรเป็นฟังก์ชันที่บริสุทธิ์โดยไม่มีผลข้างเคียง การอัพเดทร้านค้าเป็นเรื่องจริงจังและซับซ้อน อย่าปนเปื้อนมัน
  • ส่วนประกอบดูโง่? ไม่แน่นอนพวกเขามีข้อกังวลอย่างหนึ่ง: การนำเสนอและการโต้ตอบกับผู้ใช้และควรจะง่ายที่สุด
  • ส่วนประกอบคอนเทนเนอร์? เป็นไปได้ แต่ย่อยที่ดีที่สุด มันสมเหตุสมผลที่คอนเทนเนอร์เป็นสถานที่ที่เราสรุปความซับซ้อนที่เกี่ยวข้องกับมุมมองและโต้ตอบกับร้านค้า แต่:
    • ภาชนะบรรจุจะต้องมีความซับซ้อนมากกว่าองค์ประกอบที่เป็นใบ้ แต่ก็ยังเป็นความรับผิดชอบเดียว: ให้การเชื่อมระหว่างมุมมองกับรัฐ / ร้านค้า ตรรกะ async ของคุณเป็นข้อกังวลแยกจากทั้งหมด
    • ด้วยการวางลงในคอนเทนเนอร์คุณจะต้องล็อคตรรกะ async ของคุณไว้ในบริบทเดียวสำหรับมุมมอง / เส้นทางเดียว ความคิดที่ไม่ดี เป็นการดีที่มันสามารถนำมาใช้ซ้ำได้และแยกออกจากกันโดยสิ้นเชิง
  • เป็นโมดูลบริการอื่น ๆ แนวคิดที่ไม่ดี: คุณจะต้องเข้าถึงการเข้าถึงร้านค้าซึ่งเป็นฝันร้ายที่การบำรุงรักษา / การทดสอบได้ ดีกว่าไปกับธัญพืชของ Redux และเข้าถึงร้านค้าโดยใช้ API / รุ่นที่ให้ไว้เท่านั้น
  • การกระทำและ Middlewares ที่ตีความพวกเขา? ทำไมจะไม่ล่ะ?! สำหรับผู้เริ่มเป็นตัวเลือกหลักที่เราทิ้งไว้ :-) ยิ่งกว่าเหตุผลระบบการดำเนินการเป็นตรรกะการดำเนินการแยกชิ้นส่วนที่คุณสามารถใช้ได้จากทุกที่ เข้าถึงร้านค้าได้และสามารถดำเนินการเพิ่มเติมได้ มันมีความรับผิดชอบเพียงอย่างเดียวซึ่งก็คือการจัดระเบียบการควบคุมและข้อมูลรอบ ๆ แอปพลิเคชันและ async ส่วนใหญ่จะเข้ากันได้ดีกับมัน
    • ผู้สร้าง Action มีอะไรบ้าง? ทำไมไม่เพียงทำ async ในนั้นแทนที่จะทำเองและในมิดเดิลแวร์?
      • สิ่งสำคัญอันดับแรกผู้สร้างไม่มีสิทธิ์เข้าถึงร้านค้าดังเช่นมิดเดิลแวร์ นั่นหมายความว่าคุณไม่สามารถส่งการกระทำที่เกิดขึ้นใหม่ไม่สามารถอ่านได้จากร้านค้าเพื่อเขียน async ของคุณเป็นต้น
      • ดังนั้นรักษาความซับซ้อนในสถานที่ที่มีความจำเป็นและทำให้ทุกอย่างเรียบง่าย ผู้สร้างสามารถเป็นฟังก์ชั่นที่เรียบง่ายและค่อนข้างบริสุทธิ์ซึ่งง่ายต่อการทดสอบ

ส่วนประกอบคอนเทนเนอร์ - ทำไมไม่ เนื่องจากส่วนประกอบบทบาทเล่นใน React คอนเทนเนอร์อาจทำหน้าที่เป็นเซอร์วิสคลาสและได้รับการจัดเก็บผ่าน DI (อุปกรณ์ประกอบฉาก) แล้ว เมื่อวางลงในคอนเทนเนอร์คุณจะต้องล็อคตรรกะ async ของคุณไว้ในบริบทเดียวสำหรับมุมมอง / เส้นทางเดียว - อย่างไร ส่วนประกอบสามารถมีได้หลายอินสแตนซ์ มันสามารถแยกออกจากการนำเสนอเช่นกับการแสดงผลเสา ฉันเดาคำตอบจะได้ประโยชน์มากขึ้นจากตัวอย่างสั้น ๆ ที่พิสูจน์ประเด็น
Estus Flask

ฉันรักคำตอบนี้!
Mauricio Avendaño

13

ในการตอบคำถามที่ถามในตอนแรก:

ทำไมส่วนประกอบคอนเทนเนอร์ไม่สามารถเรียกใช้ async API จากนั้นส่งการดำเนินการ

โปรดทราบว่าเอกสารเหล่านั้นมีไว้สำหรับ Redux ไม่ใช่ Redux plus React ร้านค้า Redux เชื่อมต่อกับส่วนประกอบ Reactสามารถทำสิ่งที่คุณพูดได้อย่างชัดเจน แต่ร้านค้า Jane Redux ที่ไม่มีมิดเดิลแวร์ไม่ยอมรับข้อโต้แย้งdispatchยกเว้นวัตถุ ol ล้วน

หากไม่มีมิดเดิ้ลคุณก็สามารถทำได้

const store = createStore(reducer);
MyAPI.doThing().then(resp => store.dispatch(...));

แต่มันเป็นกรณีที่คล้ายกันซึ่งมีการซิงโครไนซ์ล้อมรอบ Redux แทนที่จะจัดการโดย Redux ดังนั้นตัวกลางช่วยให้ asynchrony dispatchโดยการปรับเปลี่ยนสิ่งที่สามารถส่งโดยตรงไปยัง


ที่กล่าวว่าจิตวิญญาณของข้อเสนอแนะของคุณคือฉันคิดว่าถูกต้อง มีวิธีอื่น ๆ ที่คุณสามารถจัดการแบบอะซิงโครนัสในแอปพลิเคชัน Redux + React ได้

ข้อดีอย่างหนึ่งของการใช้มิดเดิลแวร์ก็คือคุณสามารถใช้ผู้สร้างแอ็คชั่นได้ตามปกติโดยไม่ต้องกังวลว่าพวกมันจะเชื่อมต่อกันอย่างไร ตัวอย่างเช่นการใช้redux-thunkรหัสที่คุณเขียนจะมีลักษณะเหมือนกัน

function updateThing() {
  return dispatch => {
    dispatch({
      type: ActionTypes.STARTED_UPDATING
    });
    AsyncApi.getFieldValue()
      .then(result => dispatch({
        type: ActionTypes.UPDATED,
        payload: result
      }));
  }
}

const ConnectedApp = connect(
  (state) => { ...state },
  { update: updateThing }
)(App);

ซึ่งไม่ได้ดูแตกต่างไปจากต้นฉบับเลย - มันแค่สับนิดหน่อยแล้วconnectก็ไม่รู้ว่าupdateThingมันคืออะซิงโครนัส

หากคุณยังต้องการที่จะสนับสนุนสัญญา , observables , โศกนาฏกรรมหรือที่กำหนดเองบ้าและเปิดเผยสูงกรรมวิธีการดำเนินการแล้ว Redux สามารถทำมันได้เพียงแค่เปลี่ยนสิ่งที่คุณส่งผ่านไปdispatch(หรือที่รู้จักสิ่งที่คุณกลับมาจากการสร้างการดำเนินการ) ไม่ต้องทำการแมตช์ด้วยส่วนประกอบ React (หรือการconnectโทร) ที่จำเป็น


คุณแนะนำให้ส่งเหตุการณ์อื่นเมื่อเสร็จสิ้นการดำเนินการแล้ว สิ่งนี้จะไม่ทำงานเมื่อคุณต้องการแสดงการแจ้งเตือน () หลังจากเสร็จสิ้นการดำเนินการ คำมั่นสัญญาในส่วนประกอบ React ใช้งานได้ ขณะนี้ฉันแนะนำวิธีสัญญา
catamphetamine

8

ตกลงเรามาเริ่มกันดูว่ามิดเดิลแวร์ทำงานอย่างไรก่อนตอบคำถามนี้นี่คือซอร์สโค้ดของฟังก์ชั่นpplyMiddleWareใน Redux:

function applyMiddleware() {
  for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {
    middlewares[_key] = arguments[_key];
  }

  return function (createStore) {
    return function (reducer, preloadedState, enhancer) {
      var store = createStore(reducer, preloadedState, enhancer);
      var _dispatch = store.dispatch;
      var chain = [];

      var middlewareAPI = {
        getState: store.getState,
        dispatch: function dispatch(action) {
          return _dispatch(action);
        }
      };
      chain = middlewares.map(function (middleware) {
        return middleware(middlewareAPI);
      });
      _dispatch = compose.apply(undefined, chain)(store.dispatch);

      return _extends({}, store, {
        dispatch: _dispatch
      });
    };
  };
}

ดูที่ส่วนนี้ดูว่าการจัดส่งของเราเป็นหน้าที่อย่างไร

  ...
  getState: store.getState,
  dispatch: function dispatch(action) {
  return _dispatch(action);
}
  • โปรดทราบว่าแต่ละมิดเดิลแวร์จะได้รับdispatchและgetStateทำหน้าที่เป็นอาร์กิวเมนต์ที่กำหนดชื่อ

ตกลงนี่เป็นวิธีที่Redux-thunkเป็นหนึ่งในมิดเดิลแวร์ที่ใช้มากที่สุดสำหรับ Redux แนะนำตัวเอง:

มิดเดิลแวร์ Redux Thunk ช่วยให้คุณเขียนผู้สร้างแอ็คชั่นที่ส่งคืนฟังก์ชันแทนที่จะเป็นแอคชั่น thunk สามารถใช้เพื่อหน่วงเวลาการส่งของการดำเนินการหรือส่งเฉพาะในกรณีที่ตรงตามเงื่อนไขบางประการ ฟังก์ชั่นด้านในได้รับวิธีการจัดส่งและ getState เป็นพารามิเตอร์

ดังที่คุณเห็นมันจะส่งคืนฟังก์ชันแทนการกระทำหมายความว่าคุณสามารถรอและโทรหาได้ทุกเวลาที่คุณต้องการเพราะมันเป็นฟังก์ชัน ...

แล้วสิ่งที่ห่ายกระเซ็น? นี่คือวิธีการนำเสนอใน Wikipedia:

ในการเขียนโปรแกรมคอมพิวเตอร์ thunk เป็นรูทีนย่อยที่ใช้เพื่อฉีดการคำนวณเพิ่มเติมลงในรูทีนย่อยอื่น ส่วนใหญ่จะใช้ Thunks เพื่อชะลอการคำนวณจนกว่าจะจำเป็นหรือเพื่อแทรกการดำเนินการที่จุดเริ่มต้นหรือจุดสิ้นสุดของรูทีนย่อยอื่น ๆ มีแอพพลิเคชั่นอื่น ๆ อีกมากมายสำหรับการสร้างโค้ดคอมไพเลอร์และในการเขียนโปรแกรมแบบแยกส่วน

คำนี้มีต้นกำเนิดมาจากคำว่า "คิด"

thunk เป็นฟังก์ชันที่ล้อมรอบนิพจน์เพื่อหน่วงเวลาการประเมิน

//calculation of 1 + 2 is immediate 
//x === 3 
let x = 1 + 2;

//calculation of 1 + 2 is delayed 
//foo can be called later to perform the calculation 
//foo is a thunk! 
let foo = () => 1 + 2;

ดังนั้นดูว่าแนวคิดง่ายแค่ไหนและจะช่วยให้คุณจัดการแอ็คชั่น async ได้อย่างไร ...

นั่นคือสิ่งที่คุณสามารถอยู่ได้โดยปราศจากมัน แต่โปรดจำไว้ว่าในการเขียนโปรแกรมมีวิธีที่ดีกว่าเสมอ neater และวิธีที่เหมาะสมในการทำสิ่ง ...

ใช้มิดเดิลแวร์ Redux


1
ครั้งแรกที่ SO ไม่ได้อ่านอะไรเลย แต่ชอบโพสต์จ้องภาพ น่าอัศจรรย์คำใบ้และการเตือนความจำ
Bhojendra Rauniyar

2

ในการใช้ Redux-saga เป็นมิดเดิลแวร์ที่ดีที่สุดในการใช้ React-redux

เช่น store.js

  import createSagaMiddleware from 'redux-saga';
  import { createStore, applyMiddleware } from 'redux';
  import allReducer from '../reducer/allReducer';
  import rootSaga from '../saga';

  const sagaMiddleware = createSagaMiddleware();
  const store = createStore(
     allReducer,
     applyMiddleware(sagaMiddleware)
   )

   sagaMiddleware.run(rootSaga);

 export default store;

แล้ว saga.js

import {takeLatest,delay} from 'redux-saga';
import {call, put, take, select} from 'redux-saga/effects';
import { push } from 'react-router-redux';
import data from './data.json';

export function* updateLesson(){
   try{
       yield put({type:'INITIAL_DATA',payload:data}) // initial data from json
       yield* takeLatest('UPDATE_DETAIL',updateDetail) // listen to your action.js 
   }
   catch(e){
      console.log("error",e)
     }
  }

export function* updateDetail(action) {
  try{
       //To write store update details
   }  
    catch(e){
       console.log("error",e)
    } 
 }

export default function* rootSaga(){
    yield [
        updateLesson()
       ]
    }

และจากนั้น action.js

 export default function updateFruit(props,fruit) {
    return (
       {
         type:"UPDATE_DETAIL",
         payload:fruit,
         props:props
       }
     )
  }

แล้ว reducer.js

import {combineReducers} from 'redux';

const fetchInitialData = (state=[],action) => {
    switch(action.type){
      case "INITIAL_DATA":
          return ({type:action.type, payload:action.payload});
          break;
      }
     return state;
  }
 const updateDetailsData = (state=[],action) => {
    switch(action.type){
      case "INITIAL_DATA":
          return ({type:action.type, payload:action.payload});
          break;
      }
     return state;
  }
const allReducers =combineReducers({
   data:fetchInitialData,
   updateDetailsData
 })
export default allReducers; 

แล้ว main.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './app/components/App.jsx';
import {Provider} from 'react-redux';
import store from './app/store';
import createRoutes from './app/routes';

const initialState = {};
const store = configureStore(initialState, browserHistory);

ReactDOM.render(
       <Provider store={store}>
          <App />  /*is your Component*/
       </Provider>, 
document.getElementById('app'));

ลองนี้ .. ใช้งานได้


3
นี่เป็นสิ่งที่ร้ายแรงสำหรับบางคนที่ต้องการเรียกจุดสิ้นสุด API เพื่อส่งคืนเอนทิตีหรือรายการเอนทิตี คุณแนะนำว่า "ทำสิ่งนี้ ... แล้วนี่จากนั้นนี่จากนั้นนี่คือสิ่งอื่นจากนั้นก็จากนั้นอีกสิ่งนี้จากนั้นทำต่อแล้วทำ .. " แต่มนุษย์นี่คือ FRONTEND เราเพียงแค่เรียก BACKEND เพื่อให้ข้อมูลพร้อมที่จะใช้กับส่วนหน้า หากนี่เป็นวิธีที่จะไปมีบางอย่างผิดปกติมีบางอย่างผิดปกติจริง ๆ และมีคนไม่สมัคร KISS ทุกวันนี้
zameb

สวัสดีใช้ try and catch block สำหรับการโทร API เมื่อ API ได้รับการตอบสนองแล้วให้เรียกใช้ประเภทการกระทำของ Reducer
SM Chinna

1
@zameb คุณอาจพูดถูก แต่การร้องเรียนของคุณนั้นอยู่ที่ Redux และสิ่งที่ได้ยินมาทั้งหมดในขณะที่พยายามลดความซับซ้อน
jorisw

1

มีผู้สร้างแอ็คชั่นแบบซิงโครนัสจากนั้นก็มีผู้สร้างแอคชั่นแบบอะซิงโครนัส

ผู้สร้างแอคชั่นแบบซิงโครนัสคือสิ่งที่เมื่อเราเรียกมันมันจะส่งคืนออบเจกต์การกระทำทันทีพร้อมข้อมูลที่เกี่ยวข้องทั้งหมดที่แนบกับวัตถุนั้นและพร้อมที่จะประมวลผลโดยตัวลดของเรา

อะซิงโครนัสผู้สร้างแอคชั่นเป็นหนึ่งในนั้นจะต้องใช้เวลาเล็กน้อยก่อนที่มันจะพร้อมที่จะส่งการกระทำในที่สุด

ตามคำนิยามทุกครั้งที่คุณมีผู้สร้างแอคชั่นที่สร้างคำขอเครือข่ายมันจะมีคุณสมบัติเสมอในฐานะผู้สร้างแอคชั่น async

หากคุณต้องการให้ผู้สร้างแอคชั่นแบบอะซิงโครนัสภายในแอปพลิเคชัน Redux คุณต้องติดตั้งบางสิ่งที่เรียกว่ามิดเดิลแวร์ที่จะช่วยให้คุณสามารถจัดการกับผู้สร้างแอคชั่นอะซิงโครนัส

คุณสามารถตรวจสอบสิ่งนี้ได้ในข้อความแสดงข้อผิดพลาดที่บอกให้เราใช้มิดเดิลแวร์ที่กำหนดเองสำหรับการทำงานของ async

มิดเดิลแวร์คืออะไรและทำไมเราต้องใช้ async flow ใน Redux

ในบริบทของมิดเดิลแวร์ redux เช่น redux-thunk มิดเดิลแวร์ช่วยให้เราจัดการกับผู้สร้างแอคชั่นแบบอะซิงโครนัสเนื่องจากเป็นสิ่งที่ Redux ไม่สามารถจัดการได้นอกกรอบ

ด้วยมิดเดิลแวร์ที่รวมเข้ากับวัฏจักร Redux เรายังคงเรียกผู้สร้างแอ็คชั่นที่จะส่งคืนการกระทำที่จะถูกส่ง แต่ตอนนี้เมื่อเราส่งการกระทำแทนที่จะส่งมันออกไปยังอุปกรณ์ลดแรงดันโดยตรงทั้งหมดของเรา เพื่อบอกว่าการกระทำจะถูกส่งผ่านมิดเดิลแวร์ที่แตกต่างกันทั้งหมดภายในแอปพลิเคชัน

ภายในแอพ Redux เดียวเราสามารถมีมิดเดิลแวร์มากหรือน้อยตามที่เราต้องการ ส่วนใหญ่ในโครงการที่เราทำเราจะมีมิดเดิ้ลหนึ่งหรือสองตัวติดอยู่ที่ร้าน Redux ของเรา

มิดเดิลแวร์เป็นฟังก์ชัน JavaScript ธรรมดาที่จะถูกเรียกพร้อมกับทุกการกระทำที่เราจัดส่ง ภายในฟังก์ชั่นนั้นมิดเดิลแวร์มีโอกาสที่จะหยุดการกระทำจากการถูกส่งไปยัง reducers ใด ๆ ก็สามารถแก้ไขการกระทำหรือเพียงแค่ยุ่งกับการกระทำในทางที่คุณตัวอย่างเช่นเราสามารถสร้างมิดเดิลแวร์ที่คอนโซลบันทึก ทุกการกระทำที่คุณส่งไปเพียงเพื่อความสุขในการรับชมของคุณ

มีมิดเดิลแวร์โอเพนซอร์สจำนวนมหาศาลที่คุณสามารถติดตั้งเป็นการอ้างอิงในโครงการของคุณ

คุณไม่ จำกัด เฉพาะการใช้มิดเดิลแวร์โอเพนซอร์สหรือติดตั้งเป็นการอ้างอิงเท่านั้น คุณสามารถเขียนมิดเดิลแวร์แบบกำหนดเองของคุณและใช้ภายในร้าน Redux ของคุณ

หนึ่งในการใช้งานมิดเดิลแวร์ที่ได้รับความนิยมมากขึ้น (และตอบคำถามของคุณ) คือการจัดการกับผู้สร้างแอคซิงโครนัสซึ่งอาจเป็นมิดเดิลแวร์ที่ได้รับความนิยมมากที่สุดคือ redux-thunk และช่วยคุณจัดการกับแอคชั่น

มิดเดิลแวร์ประเภทอื่น ๆ อีกมากมายที่ช่วยคุณในการจัดการกับผู้สร้างแอคชั่นไม่ตรงกัน


1

เพื่อตอบคำถาม:

ทำไมส่วนประกอบคอนเทนเนอร์ไม่สามารถเรียกใช้ async API จากนั้นส่งการดำเนินการ

ฉันจะพูดด้วยเหตุผลอย่างน้อยสองประการ:

เหตุผลแรกคือการแยกของความกังวลก็ไม่ได้เป็นงานของaction creatorการเรียกapiและได้รับข้อมูลกลับมาคุณจะต้องมีการส่งผ่านสองอาร์กิวเมนต์ของคุณaction creator functionที่และaction typepayload

เหตุผลที่สองเนื่องจากredux storeกำลังรอวัตถุธรรมดาที่มีประเภทการกระทำที่บังคับและเป็นทางเลือก a payload(แต่ที่นี่คุณต้องผ่าน payload ด้วย)

ผู้สร้างแอคชั่นควรเป็นวัตถุธรรมดาดังนี้:

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}

และการปฏิบัติงานของRedux-Thunk midlewareการdispacheผลมาจากการที่คุณจะเหมาะสมapi callaction


0

เมื่อทำงานในโครงการขององค์กรมีข้อกำหนดมากมายที่มีอยู่ในมิดเดิลแวร์เช่น (saga) ที่ไม่พร้อมใช้งานในการไหลแบบอะซิงโครนัสอย่างง่ายด้านล่างคือ:

  • การเรียกใช้คำขอแบบขนาน
  • ดึงการกระทำในอนาคตโดยไม่จำเป็นต้องรอ
  • การโทรแบบไม่มีการบล็อกเอฟเฟกต์การแข่งขันตัวอย่างก่อนเลือก
  • การตอบสนองเพื่อเริ่มกระบวนการเรียงลำดับงานของคุณ (สายแรก)
  • เขียน
  • การยกเลิกงานการฟอร์กงาน
  • สนับสนุน Concurrency Running Saga นอกมิดเดิลแวร์การเปลี่ยนค่า
  • ใช้ช่องทาง

รายการมีความยาวเพียงตรวจสอบส่วนขั้นสูงในเอกสารประกอบเทพนิยาย

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.