จะจัดส่งการกระทำ Redux ด้วยการหมดเวลาได้อย่างไร


891

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

ฉันไม่มีโชคกับการใช้งานsetTimeoutและกลับมาดำเนินการอีกครั้งและไม่สามารถหาวิธีการออนไลน์ได้ ดังนั้นคำแนะนำใด ๆ ยินดีต้อนรับ


30
อย่าลืมตรวจสอบredux-sagaคำตอบของฉันหากคุณต้องการสิ่งที่ดีกว่า thunks คำตอบล่าช้าดังนั้นคุณต้องเลื่อนเป็นเวลานานก่อนที่จะเห็นมันปรากฏ :) ไม่ได้หมายความว่ามันไม่คุ้มค่าที่จะอ่าน นี่คือทางลัด: stackoverflow.com/a/38574266/82609
Sebastien Lorber

5
เมื่อใดก็ตามที่คุณทำ setTimeout อย่าลืมล้างตัวจับเวลาโดยใช้ clearTimeout ใน componentWillUnMount life cycle method
Hemadri Dasari

2
redux-saga เจ๋ง แต่พวกมันดูเหมือนจะไม่ได้รับการสนับสนุนสำหรับคำตอบที่พิมพ์จากฟังก์ชันตัวสร้าง อาจสำคัญหากคุณใช้ typescript พร้อมการตอบสนอง
Crhistian Ramirez

คำตอบ:


2616

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

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

การเขียน Async Code Inline

นี่เป็นวิธีที่ง่ายที่สุด และไม่มีอะไรพิเศษสำหรับ Redux ที่นี่

store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

ในทำนองเดียวกันจากภายในองค์ประกอบที่เชื่อมต่อ:

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  this.props.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

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

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

// actions.js
export function showNotification(text) {
  return { type: 'SHOW_NOTIFICATION', text }
}
export function hideNotification() {
  return { type: 'HIDE_NOTIFICATION' }
}

// component.js
import { showNotification, hideNotification } from '../actions'

this.props.dispatch(showNotification('You just logged in.'))
setTimeout(() => {
  this.props.dispatch(hideNotification())
}, 5000)

หรือหากคุณเคยผูกมัดไว้กับconnect():

this.props.showNotification('You just logged in.')
setTimeout(() => {
  this.props.hideNotification()
}, 5000)

จนถึงตอนนี้เรายังไม่ได้ใช้มิดเดิลแวร์หรือแนวคิดขั้นสูงอื่น ๆ

แยก Async Action Creator

วิธีการดังกล่าวใช้งานได้ดีในกรณีง่าย ๆ แต่คุณอาจพบว่ามีปัญหาเล็กน้อย:

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

ในการแก้ปัญหาเหล่านี้คุณจะต้องแยกฟังก์ชั่นที่รวมศูนย์ตรรกะการหมดเวลาและส่งการกระทำทั้งสอง อาจมีลักษณะเช่นนี้:

// actions.js
function showNotification(id, text) {
  return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
  return { type: 'HIDE_NOTIFICATION', id }
}

let nextNotificationId = 0
export function showNotificationWithTimeout(dispatch, text) {
  // Assigning IDs to notifications lets reducer ignore HIDE_NOTIFICATION
  // for the notification that is not currently visible.
  // Alternatively, we could store the timeout ID and call
  // clearTimeout(), but we’d still want to do it in a single place.
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

  setTimeout(() => {
    dispatch(hideNotification(id))
  }, 5000)
}

ขณะนี้ส่วนประกอบสามารถใช้งานได้showNotificationWithTimeoutโดยไม่ต้องทำซ้ำตรรกะนี้หรือมีสภาพการแข่งขันพร้อมการแจ้งเตือนที่แตกต่างกัน:

// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')    

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

หากคุณมีร้านค้าแบบซิงเกิลตันที่ส่งออกจากโมดูลบางตัวคุณสามารถนำเข้าและนำเข้าdispatchโดยตรงได้ที่:

// store.js
export default createStore(reducer)

// actions.js
import store from './store'

// ...

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  const id = nextNotificationId++
  store.dispatch(showNotification(id, text))

  setTimeout(() => {
    store.dispatch(hideNotification(id))
  }, 5000)
}

// component.js
showNotificationWithTimeout('You just logged in.')

// otherComponent.js
showNotificationWithTimeout('You just logged out.')    

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

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

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

กลับไปเป็นรุ่นก่อนหน้า:

// actions.js

// ...

let nextNotificationId = 0
export function showNotificationWithTimeout(dispatch, text) {
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

  setTimeout(() => {
    dispatch(hideNotification(id))
  }, 5000)
}

// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')    

วิธีนี้จะช่วยแก้ปัญหาด้วยการทำซ้ำของตรรกะและช่วยเราจากสภาวะการแข่งขัน

Thunk Middleware

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

อย่างไรก็ตามในแอปขนาดใหญ่คุณอาจพบว่ามีความไม่สะดวกอยู่รอบตัว

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

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

นี่คือแรงจูงใจในการค้นหาวิธีการ“ ทำให้ถูกต้องตามกฎหมาย” รูปแบบของการให้dispatchฟังก์ชั่นผู้ช่วยนี้และช่วย Redux“ ดู” ผู้สร้างแอ็คชั่นอะซิงโครนัสเช่นกรณีพิเศษของผู้สร้างแอ็คชั่นทั่วไป

หากคุณยังอยู่กับเราและคุณก็รู้ว่าเป็นปัญหาในแอปของคุณคุณสามารถใช้มิดเดิลแวร์Redux Thunkได้

สรุปสาระสำคัญ Redux Thunk สอนให้ Redux รู้จักการกระทำแบบพิเศษที่มีอยู่ในฟังก์ชั่นจริง:

import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'

const store = createStore(
  reducer,
  applyMiddleware(thunk)
)

// It still recognizes plain object actions
store.dispatch({ type: 'INCREMENT' })

// But with thunk middleware, it also recognizes functions
store.dispatch(function (dispatch) {
  // ... which themselves may dispatch many times
  dispatch({ type: 'INCREMENT' })
  dispatch({ type: 'INCREMENT' })
  dispatch({ type: 'INCREMENT' })

  setTimeout(() => {
    // ... even asynchronously!
    dispatch({ type: 'DECREMENT' })
  }, 1000)
})

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

มันไม่ได้ดูมีประโยชน์มากใช่ไหม? ไม่ได้อยู่ในสถานการณ์นี้โดยเฉพาะ อย่างไรก็ตามมันช่วยให้เราประกาศshowNotificationWithTimeout()เป็น Redux action creator:

// actions.js
function showNotification(id, text) {
  return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
  return { type: 'HIDE_NOTIFICATION', id }
}

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  return function (dispatch) {
    const id = nextNotificationId++
    dispatch(showNotification(id, text))

    setTimeout(() => {
      dispatch(hideNotification(id))
    }, 5000)
  }
}

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

เราจะใช้มันอย่างไรในองค์ประกอบของเรา แน่นอนเราสามารถเขียนสิ่งนี้:

// component.js
showNotificationWithTimeout('You just logged in.')(this.props.dispatch)

เราจะเรียกร้องให้ผู้สร้างการกระทำ async ที่จะได้รับฟังก์ชั่นภายในที่ต้องการเพียงและจากนั้นเราจะผ่านdispatchdispatch

อย่างไรก็ตามนี่มันน่าอึดอัดใจกว่าเวอร์ชั่นดั้งเดิมมาก! ทำไมเราถึงไปทางนั้น

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

ดังนั้นเราสามารถทำสิ่งนี้แทน:

// component.js
this.props.dispatch(showNotificationWithTimeout('You just logged in.'))

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

โปรดสังเกตว่าเนื่องจากเรา“ สอน” ให้ Redux รู้จักผู้สร้างแอ็คชั่น“ พิเศษ” (เราเรียกพวกเขาว่าผู้สร้างแอ็คชั่นอันธพาล ) ตอนนี้เราสามารถใช้พวกมันได้ทุกที่ที่เราจะใช้ผู้สร้างแอ็คชั่นทั่วไป ตัวอย่างเช่นเราสามารถใช้พวกเขากับconnect():

// actions.js

function showNotification(id, text) {
  return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
  return { type: 'HIDE_NOTIFICATION', id }
}

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  return function (dispatch) {
    const id = nextNotificationId++
    dispatch(showNotification(id, text))

    setTimeout(() => {
      dispatch(hideNotification(id))
    }, 5000)
  }
}

// component.js

import { connect } from 'react-redux'

// ...

this.props.showNotificationWithTimeout('You just logged in.')

// ...

export default connect(
  mapStateToProps,
  { showNotificationWithTimeout }
)(MyComponent)

อ่านรัฐใน Thunks

โดยปกติ reducers ของคุณจะมีตรรกะทางธุรกิจสำหรับการกำหนดสถานะต่อไป อย่างไรก็ตาม reducers จะเตะเข้าหลังจากการกระทำถูกส่งออกไป จะเป็นอย่างไรถ้าคุณมีผลข้างเคียง (เช่นเรียก API) ในผู้สร้างแอ็คชั่น thunk และคุณต้องการป้องกันมันภายใต้เงื่อนไขบางอย่าง?

โดยไม่ใช้มิดเดิลแวร์ thunk คุณเพียงแค่ทำเครื่องหมายนี้ในส่วนประกอบ:

// component.js
if (this.props.areNotificationsEnabled) {
  showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
}

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

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  return function (dispatch, getState) {
    // Unlike in a regular action creator, we can exit early in a thunk
    // Redux doesn’t care about its return value (or lack of it)
    if (!getState().areNotificationsEnabled) {
      return
    }

    const id = nextNotificationId++
    dispatch(showNotification(id, text))

    setTimeout(() => {
      dispatch(hideNotification(id))
    }, 5000)
  }
}

อย่าใช้รูปแบบนี้ในทางที่ผิด เป็นการดีสำหรับการประกันตัวจากการเรียกใช้ API เมื่อมีข้อมูลแคชอยู่ แต่ไม่ได้เป็นรากฐานที่ดีในการสร้างตรรกะทางธุรกิจของคุณ หากคุณใช้getState()เพื่อส่งการกระทำที่แตกต่างตามเงื่อนไขให้พิจารณาวางตรรกะทางธุรกิจลงในตัวลดแทน

ขั้นตอนถัดไป

ตอนนี้คุณมีสัญชาตญาณพื้นฐานเกี่ยวกับวิธีการทำงานของ Thunks ลองดูตัวอย่าง Redux asyncที่ใช้พวกมัน

คุณอาจพบตัวอย่างมากมายที่ thunks ส่งคืนสัญญา สิ่งนี้ไม่จำเป็น แต่สะดวกมาก Redux ไม่สนใจสิ่งที่คุณกลับมาจาก thunk dispatch()แต่มันช่วยให้คุณมีค่าตอบแทนจาก นี่คือเหตุผลที่คุณสามารถกลับสัญญาจาก thunk dispatch(someThunkReturningPromise()).then(...)และรอให้เสร็จสมบูรณ์โดยการโทร

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

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

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

store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

อย่าเหงื่อมันจนกว่าคุณจะรู้ว่าทำไมคุณทำเช่นนี้


27
การทำงานของ Async ดูเหมือนจะเป็นทางออกที่เรียบง่ายและสง่างามสำหรับปัญหาทั่วไป เหตุใดจึงไม่สนับสนุนพวกเขาที่อบเข้าสู่ redux โดยไม่ต้องใช้มิดเดิลแวร์ คำตอบนี้อาจจะกระชับมากขึ้น
Phil Mander

83
@PhilMander เนื่องจากมีรูปแบบทางเลือกมากมายเช่นgithub.com/raisemarketplace/redux-loopหรือgithub.com/yelouafi/redux-sagaซึ่งดูสง่างาม (ถ้าไม่มาก) Redux เป็นเครื่องมือระดับต่ำ คุณสามารถสร้างซูเปอร์เซ็ตที่คุณชอบและแจกจ่ายแยกต่างหาก
Dan Abramov

16
คุณสามารถอธิบายสิ่งนี้ได้อย่างไร: * พิจารณาการวางตรรกะทางธุรกิจลงใน reducers * นั่นหมายความว่าฉันควรจัดส่งการกระทำแล้วพิจารณาในตัวลดสิ่งที่ต้องดำเนินการเพิ่มเติมเพื่อจัดส่งขึ้นอยู่กับสถานะของฉัน คำถามของฉันคือฉันจะจัดส่งการกระทำอื่น ๆ โดยตรงในตัวลดของฉันและถ้าไม่แล้วฉันจะส่งพวกเขาจากที่ไหน?
froginvasion

25
ประโยคนี้ใช้กับกรณีซิงโครนัสเท่านั้น ตัวอย่างเช่นถ้าคุณเขียนif (cond) dispatch({ type: 'A' }) else dispatch({ type: 'B' })บางทีคุณควรdispatch({ type: 'C', something: cond })และเลือกที่จะไม่สนใจการกระทำใน reducers แทนขึ้นอยู่กับaction.somethingสถานะปัจจุบัน
Dan Abramov

29
@DanAbramov คุณได้รับ upvote ของฉันเพียงแค่นี้ "ถ้าคุณไม่มีปัญหานี้ใช้สิ่งที่ภาษาเสนอและไปหาทางออกที่ง่ายที่สุด" หลังจากนั้นฉันก็รู้ว่าใครเป็นคนเขียนมัน!
Matt Lacey

189

ใช้ Redux-saga

ในฐานะที่เป็นแดน Abramov กล่าวว่าถ้าคุณต้องการการควบคุมที่สูงขึ้นกว่ารหัส async ของคุณคุณอาจจะดูที่Redux-เทพนิยาย

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

แนวคิดทั่วไปคือ Redux-saga เสนอล่ามเครื่องกำเนิด ES6 ที่อนุญาตให้คุณเขียนโค้ด async ที่ดูเหมือนรหัสซิงโครนัสได้ง่าย (นี่คือเหตุผลที่คุณมักพบว่าไม่มีที่สิ้นสุด อย่างใด Redux-saga กำลังสร้างภาษาของตัวเองโดยตรงภายใน Javascript Redux-saga สามารถเรียนรู้ได้ยากในตอนแรกเพราะคุณต้องการความเข้าใจพื้นฐานของเครื่องกำเนิดไฟฟ้า แต่ยังเข้าใจภาษาที่ Redux-saga นำเสนอด้วย

ฉันจะลองที่นี่เพื่ออธิบายที่นี่ระบบการแจ้งเตือนที่ฉันสร้างขึ้นบน redux-saga ตัวอย่างนี้ทำงานในการผลิตในปัจจุบัน

ข้อกำหนดระบบการแจ้งเตือนขั้นสูง

  • คุณสามารถขอให้แสดงการแจ้งเตือนได้
  • คุณสามารถร้องขอการแจ้งเตือนเพื่อซ่อน
  • การแจ้งเตือนไม่ควรแสดงเกิน 4 วินาที
  • สามารถแสดงการแจ้งเตือนหลายรายการพร้อมกันได้
  • ไม่สามารถแสดงการแจ้งเตือนมากกว่า 3 รายการในเวลาเดียวกัน
  • หากมีการร้องขอการแจ้งเตือนในขณะที่มีการแจ้งเตือนที่แสดงอยู่ 3 รายการให้จัดคิว / เลื่อนออกไป

ผลลัพธ์

สกรีนช็อตของแอปผลิตของฉันStample.co

ขนมปังปิ้ง

รหัส

ที่นี่ฉันตั้งชื่อการแจ้งเตือน a toastแต่นี่เป็นรายละเอียดการตั้งชื่อ

function* toastSaga() {

    // Some config constants
    const MaxToasts = 3;
    const ToastDisplayTime = 4000;


    // Local generator state: you can put this state in Redux store
    // if it's really important to you, in my case it's not really
    let pendingToasts = []; // A queue of toasts waiting to be displayed
    let activeToasts = []; // Toasts currently displayed


    // Trigger the display of a toast for 4 seconds
    function* displayToast(toast) {
        if ( activeToasts.length >= MaxToasts ) {
            throw new Error("can't display more than " + MaxToasts + " at the same time");
        }
        activeToasts = [...activeToasts,toast]; // Add to active toasts
        yield put(events.toastDisplayed(toast)); // Display the toast (put means dispatch)
        yield call(delay,ToastDisplayTime); // Wait 4 seconds
        yield put(events.toastHidden(toast)); // Hide the toast
        activeToasts = _.without(activeToasts,toast); // Remove from active toasts
    }

    // Everytime we receive a toast display request, we put that request in the queue
    function* toastRequestsWatcher() {
        while ( true ) {
            // Take means the saga will block until TOAST_DISPLAY_REQUESTED action is dispatched
            const event = yield take(Names.TOAST_DISPLAY_REQUESTED);
            const newToast = event.data.toastData;
            pendingToasts = [...pendingToasts,newToast];
        }
    }


    // We try to read the queued toasts periodically and display a toast if it's a good time to do so...
    function* toastScheduler() {
        while ( true ) {
            const canDisplayToast = activeToasts.length < MaxToasts && pendingToasts.length > 0;
            if ( canDisplayToast ) {
                // We display the first pending toast of the queue
                const [firstToast,...remainingToasts] = pendingToasts;
                pendingToasts = remainingToasts;
                // Fork means we are creating a subprocess that will handle the display of a single toast
                yield fork(displayToast,firstToast);
                // Add little delay so that 2 concurrent toast requests aren't display at the same time
                yield call(delay,300);
            }
            else {
                yield call(delay,50);
            }
        }
    }

    // This toast saga is a composition of 2 smaller "sub-sagas" (we could also have used fork/spawn effects here, the difference is quite subtile: it depends if you want toastSaga to block)
    yield [
        call(toastRequestsWatcher),
        call(toastScheduler)
    ]
}

และตัวลด:

const reducer = (state = [],event) => {
    switch (event.name) {
        case Names.TOAST_DISPLAYED:
            return [...state,event.data.toastData];
        case Names.TOAST_HIDDEN:
            return _.without(state,event.data.toastData);
        default:
            return state;
    }
};

การใช้

คุณสามารถส่งTOAST_DISPLAY_REQUESTEDเหตุการณ์ หากคุณส่งคำขอ 4 ครั้งจะมีการแจ้งเตือนเพียง 3 รายการเท่านั้นและคำขอที่ 4 จะปรากฏขึ้นเล็กน้อยในภายหลังเมื่อการแจ้งเตือนครั้งที่ 1 หายไป

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

ข้อสรุป

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

มันค่อนข้างง่ายที่จะใช้กฎที่ซับซ้อนกว่าเช่น:

  • เมื่อการแจ้งเตือนมากเกินไปคือ "เข้าคิว" ให้แสดงเวลาน้อยลงสำหรับการแจ้งเตือนแต่ละครั้งเพื่อให้ขนาดคิวสามารถลดลงได้เร็วขึ้น
  • ตรวจจับการเปลี่ยนแปลงขนาดหน้าต่างและเปลี่ยนจำนวนสูงสุดของการแจ้งเตือนที่แสดง (เช่นเดสก์ท็อป = 3, แนวตั้งโทรศัพท์ = 2, แนวนอนโทรศัพท์ = 1)

ขอให้โชคดีในการนำสิ่งเหล่านี้มาใช้ได้อย่างเหมาะสมกับ thunks

หมายเหตุคุณสามารถทำสิ่งเดียวกันกับredux-observableซึ่งคล้ายกับ redux-saga มาก มันเกือบจะเหมือนกันและเป็นเรื่องของรสนิยมระหว่างเครื่องกำเนิดและ RxJS


18
ฉันหวังว่าคำตอบของคุณจะมาก่อนหน้านี้เมื่อมีการถามคำถามเพราะฉันไม่เห็นด้วยกับการใช้ห้องสมุดผลข้างเคียงของ Saga สำหรับตรรกะทางธุรกิจเช่นนี้ Reducers & Action Builders ใช้สำหรับการเปลี่ยนสถานะ เวิร์กโฟลว์ไม่เหมือนกับฟังก์ชันการเปลี่ยนสถานะ เวิร์กโฟลว์ก้าวผ่านการเปลี่ยนภาพ แต่ไม่ใช่การเปลี่ยนภาพด้วยตนเอง Redux + React ขาดสิ่งนี้ด้วยตัวเอง - นี่คือเหตุผลที่ Redux Saga มีประโยชน์มาก
Atticus

4
ขอบคุณฉันพยายามทำให้ดีที่สุดเพื่อให้ redux-saga เป็นที่นิยมด้วยเหตุผลเหล่านี้ :) มีคนน้อยคนที่คิดว่า redux-saga เป็นเพียงการแทนที่ thunks และไม่เห็นว่า redux-saga ช่วยให้เวิร์กโฟลว์ที่ซับซ้อนและแยกได้อย่างไร
Sebastien Lorber

1
เผง การดำเนินการและลดทั้งหมดเป็นส่วนหนึ่งของเครื่องรัฐ บางครั้งสำหรับเวิร์กโฟลว์ที่ซับซ้อนคุณต้องมีอย่างอื่นเพื่อจัดทำเครื่องสถานะที่ไม่ได้เป็นส่วนหนึ่งของเครื่องรัฐโดยตรง!
Atticus

2
การกระทำ: Payloads / events ไปสู่สถานะการเปลี่ยนแปลง Reducers: ฟังก์ชั่นการเปลี่ยนสถานะ ส่วนประกอบ: ส่วนต่อประสานผู้ใช้ที่สะท้อนสถานะ แต่มีชิ้นส่วนหนึ่งที่ขาดหายไป - คุณจะจัดการกระบวนการของช่วงการเปลี่ยนภาพจำนวนมากที่ทั้งหมดมีตรรกะของตัวเองที่กำหนดว่าช่วงการเปลี่ยนภาพใดที่จะดำเนินการต่อไป Red Saga!
Atticus

2
@mrbrdo ถ้าคุณอ่านคำตอบของฉันอย่างถี่ถ้วนคุณจะสังเกตเห็นว่าหมดเวลาการแจ้งเตือนจริง ๆ แล้วจัดการกับyield call(delay,timeoutValue);: ไม่ใช่ API เดียวกัน แต่มีผลเหมือนกัน
Sebastien Lorber

25

พื้นที่เก็บข้อมูลที่มีตัวอย่างโครงการ

ปัจจุบันมีโครงการตัวอย่างสี่โครงการ:

  1. การเขียน Async Code Inline
  2. แยก Async Action Creator
  3. ใช้ Redux Thunk
  4. ใช้ Redux Saga

คำตอบที่ยอมรับนั้นยอดเยี่ยม

แต่มีบางอย่างขาดหายไป:

  1. ไม่มีโปรเจ็กต์ตัวอย่างที่รันได้ซึ่งมีเพียงโค้ดบางส่วน
  2. ไม่มีรหัสตัวอย่างสำหรับทางเลือกอื่นเช่น:
    1. Redux Saga

ดังนั้นฉันจึงสร้างที่เก็บHello Asyncเพื่อเพิ่มสิ่งที่ขาดหายไป:

  1. โครงการที่เรียกใช้ คุณสามารถดาวน์โหลดและรันโดยไม่ต้องดัดแปลง
  2. ระบุโค้ดตัวอย่างสำหรับทางเลือกเพิ่มเติม:

Redux Saga

คำตอบที่ยอมรับแล้วมีตัวอย่างโค้ดสำหรับ Async Code Inline, Async Action Generator และ Redux Thunk เพื่อความสมบูรณ์ฉันได้ให้ข้อมูลโค้ดสำหรับ Redux Saga:

// actions.js

export const showNotification = (id, text) => {
  return { type: 'SHOW_NOTIFICATION', id, text }
}

export const hideNotification = (id) => {
  return { type: 'HIDE_NOTIFICATION', id }
}

export const showNotificationWithTimeout = (text) => {
  return { type: 'SHOW_NOTIFICATION_WITH_TIMEOUT', text }
}

การกระทำนั้นเรียบง่ายและบริสุทธิ์

// component.js

import { connect } from 'react-redux'

// ...

this.props.showNotificationWithTimeout('You just logged in.')

// ...

export default connect(
  mapStateToProps,
  { showNotificationWithTimeout }
)(MyComponent)

ไม่มีอะไรพิเศษกับส่วนประกอบ

// sagas.js

import { takeEvery, delay } from 'redux-saga'
import { put } from 'redux-saga/effects'
import { showNotification, hideNotification } from './actions'

// Worker saga
let nextNotificationId = 0
function* showNotificationWithTimeout (action) {
  const id = nextNotificationId++
  yield put(showNotification(id, action.text))
  yield delay(5000)
  yield put(hideNotification(id))
}

// Watcher saga, will invoke worker saga above upon action 'SHOW_NOTIFICATION_WITH_TIMEOUT'
function* notificationSaga () {
  yield takeEvery('SHOW_NOTIFICATION_WITH_TIMEOUT', showNotificationWithTimeout)
}

export default notificationSaga

Sagas ใช้เครื่องกำเนิดไฟฟ้า ES6

// index.js

import createSagaMiddleware from 'redux-saga'
import saga from './sagas'

const sagaMiddleware = createSagaMiddleware()

const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
)

sagaMiddleware.run(saga)

เปรียบเทียบกับ Redux Thunk

ข้อดี

  • คุณไม่ต้องตกนรกในการติดต่อกลับ
  • คุณสามารถทดสอบกระแสอะซิงโครนัสของคุณได้อย่างง่ายดาย
  • การกระทำของคุณบริสุทธิ์

จุดด้อย

  • มันขึ้นอยู่กับเครื่องกำเนิดไฟฟ้า ES6 ซึ่งค่อนข้างใหม่

โปรดอ้างอิงถึงโครงการที่รันได้หากโค้ดขนาดเล็กด้านบนไม่ตอบคำถามของคุณทั้งหมด


23

คุณสามารถทำเช่นนี้กับRedux-thunk มีคำแนะนำในเอกสาร reduxสำหรับการทำงานของ async เช่น setTimeout


เพียงคำถามการติดตามอย่างรวดเร็วเมื่อใช้มิดเดิลแวร์applyMiddleware(ReduxPromise, thunk)(createStore)นี่คือวิธีที่คุณเพิ่มมิดเดิลแวร์หลายตัว (คั่นด้วยอาการโคม่า) เพราะฉันดูเหมือนจะไม่สามารถทำงานได้ดี
Ilja

1
@Ilja สิ่งนี้ควรใช้งานได้:const store = createStore(reducer, applyMiddleware([ReduxPromise, thunk]));
geniuscarrier

22

ฉันจะแนะนำยังการดูที่เป็นรูปแบบ SAM

รูปแบบ SAM สนับสนุนให้รวมการกระทำ "next-action-predicate" ซึ่งการกระทำ (อัตโนมัติ) เช่น "การแจ้งเตือนจะหายไปโดยอัตโนมัติหลังจาก 5 วินาที" จะถูกเรียกใช้เมื่อแบบจำลองได้รับการปรับปรุง (รุ่น SAM ~ รัฐลด + ร้านค้า)

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

ตัวอย่างเช่นรหัส

export function showNotificationWithTimeout(dispatch, text) {
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

  setTimeout(() => {
    dispatch(hideNotification(id))
  }, 5000)
}

จะไม่ได้รับอนุญาตกับ SAM เนื่องจากความจริงที่ว่าการดำเนินการ hideNotification สามารถส่งขึ้นอยู่กับรุ่นที่ยอมรับค่า "showNotication: true" สำเร็จ อาจมีส่วนอื่น ๆ ของโมเดลที่ป้องกันไม่ให้ยอมรับและดังนั้นจึงไม่มีเหตุผลที่จะทริกเกอร์การดำเนินการ hideNotification

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

คุณสามารถเข้าร่วมกับเราใน Gitter หากคุณต้องการ นอกจากนี้ยังมีคู่มือเริ่มต้นใช้งาน SAM ที่นี่เช่นกัน


ฉันมีรอยขีดข่วนบนพื้นผิวจนถึงขณะนี้ แต่ฉันก็รู้สึกตื่นเต้นกับลวดลาย SAM V = S( vm( M.present( A(data) ) ), nap(M))สวยมาก ขอบคุณสำหรับการแบ่งปันความคิดและประสบการณ์ของคุณ ฉันจะขุดลึกลงไป

@ftor ขอขอบคุณ! เมื่อฉันเขียนมันครั้งแรกฉันมีความรู้สึกเดียวกัน ฉันใช้ SAM ในการผลิตเป็นเวลาเกือบหนึ่งปีแล้วและฉันไม่สามารถนึกถึงเวลาที่ฉันรู้สึกว่าฉันต้องการห้องสมุดเพื่อใช้ SAM (แม้แต่ vdom แม้ว่าฉันจะเห็นว่ามันจะถูกใช้เมื่อใด) แค่โค้ดบรรทัดเดียวนั่นแหล่ะ! SAM สร้างรหัส isomorphic ไม่มีความคลุมเครือที่จะจัดการกับการโทรแบบ async ได้ ... ฉันไม่สามารถนึกถึงเวลาที่ฉันคิดได้ฉันกำลังทำอะไรอยู่
metaprogrammer

SAM เป็นรูปแบบวิศวกรรมซอฟต์แวร์ที่แท้จริง (เพิ่งผลิต Alexa SDK ด้วย) มันขึ้นอยู่กับ TLA + และพยายามนำพลังของงานที่เหลือเชื่อนั้นมาสู่นักพัฒนาทุกคน SAM แก้ไขการประมาณสามอย่างที่เกือบทุกคนใช้มานานหลายทศวรรษ: - การกระทำสามารถจัดการสถานะแอปพลิเคชัน - การมอบหมายเทียบเท่ากับการกลายพันธุ์ - ไม่มีคำจำกัดความที่ชัดเจนของขั้นตอนการเขียนโปรแกรม (เช่น = b * ca ขั้นตอน คือ 1 / อ่าน b, c 2 / คำนวณ b * c, 3 / กำหนด a ด้วยผลลัพธ์สามขั้นตอนที่ต่างกันหรือไม่
metaprogrammer

20

หลังจากที่พยายามวิธีที่นิยมต่างๆ (ผู้สร้างการกระทำ thunks, ตำนานมหากาพย์ผลตัวกลางที่กำหนดเอง) ผมยังคงรู้สึกว่าอาจจะมีการปรับปรุงห้องดังนั้นฉันเอกสารการเดินทางของฉันในบทความบล็อกนี้ฉันจะใส่ตรรกะทางธุรกิจของฉันใน แอปพลิเคชัน React / Redux หรือไม่

เหมือนกับการอภิปรายที่นี่ฉันพยายามเปรียบเทียบและเปรียบเทียบวิธีการต่าง ๆ ในที่สุดมันก็นำฉันไปสู่การแนะนำredux- library ไลบรารี่ใหม่ซึ่งรับแรงบันดาลใจจาก epics, sagas, มิดเดิลแวร์แบบกำหนดเอง

อนุญาตให้คุณดักจับการกระทำเพื่อตรวจสอบตรวจสอบรับรองและให้วิธีการทำ async IO

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

นั่นทำให้คุณสามารถใช้ตรรกะทางธุรกิจหลักของคุณได้ตามต้องการ คุณไม่จำเป็นต้องใช้สิ่งที่สังเกตได้หรือเครื่องกำเนิดไฟฟ้าเว้นแต่ว่าคุณต้องการ ใช้ฟังก์ชั่นและการโทรกลับ, สัญญา, ฟังก์ชั่น async (async / await) ฯลฯ

รหัสสำหรับการแจ้งเตือน 5s อย่างง่ายจะเป็นดังนี้:

const notificationHide = createLogic({
  // the action type that will trigger this logic
  type: 'NOTIFICATION_DISPLAY',
  
  // your business logic can be applied in several
  // execution hooks: validate, transform, process
  // We are defining our code in the process hook below
  // so it runs after the action hit reducers, hide 5s later
  process({ getState, action }, dispatch) {
    setTimeout(() => {
      dispatch({ type: 'NOTIFICATION_CLEAR' });
    }, 5000);
  }
});
    

ฉันมีตัวอย่างการแจ้งเตือนขั้นสูงเพิ่มเติมใน repo ของฉันที่ทำงานคล้ายกับที่ Sebastian Lorber อธิบายว่าคุณสามารถ จำกัด การแสดงผลเป็นรายการ N และหมุนผ่านรายการใด ๆ ที่อยู่ในคิว ตัวอย่างการแจ้งเตือนของ redux-logic

ฉันมีความหลากหลายของRedux ตรรกะตัวอย่างชีวิต jsfiddle เช่นเดียวกับตัวอย่างเต็มรูปแบบ ฉันยังคงทำงานกับเอกสารและตัวอย่างต่อไป

ฉันชอบที่จะได้ยินความคิดเห็นของคุณ


ฉันไม่แน่ใจว่าฉันชอบห้องสมุดของคุณ แต่ฉันชอบบทความของคุณ! ทำได้ดีมากมนุษย์! คุณทำงานได้เพียงพอที่จะช่วยประหยัดเวลาของผู้อื่น
ไทเลอร์ลอง

2
ฉันสร้างโครงการตัวอย่างสำหรับ redux-logic ที่นี่: github.com/tylerlong/hello-async/tree/master/redux-logic ฉันคิดว่ามันเป็นซอฟต์แวร์ที่มีการออกแบบมาอย่างดีและฉันไม่เห็นข้อเสียที่สำคัญใด ๆ เมื่อเทียบกับที่อื่น ทางเลือก
ไทเลอร์ลอง

9

ฉันเข้าใจว่าคำถามนี้ค่อนข้างเก่า แต่ฉันจะแนะนำโซลูชันอื่น ๆ โดยใช้Redux-observable aka มหากาพย์

การอ้างอิงเอกสารอย่างเป็นทางการ:

อะไรคือ Redux-observable

มิดเดิลแวร์ที่ใช้ RxJS 5 สำหรับ Redux เขียนและยกเลิกการทำงานแบบ async เพื่อสร้างผลข้างเคียงและอีกมากมาย

Epic นั้นเป็นแกนกลางดั้งเดิมของ Redux-observable

มันเป็นฟังก์ชั่นที่ใช้กระแสการกระทำและส่งคืนกระแสการกระทำ การกระทำในการกระทำออกมา

ในคำมากกว่าหรือน้อยกว่าคุณสามารถสร้างฟังก์ชั่นที่ได้รับการกระทำผ่านกระแสและจากนั้นกลับกระแสของการกระทำใหม่ (ใช้ผลข้างเคียงที่พบบ่อยเช่นหมดเวลาล่าช้าช่วงเวลาและคำขอ)

ให้ฉันโพสต์รหัสแล้วอธิบายอีกเล็กน้อยเกี่ยวกับมัน

store.js

import {createStore, applyMiddleware} from 'redux'
import {createEpicMiddleware} from 'redux-observable'
import {Observable} from 'rxjs'
const NEW_NOTIFICATION = 'NEW_NOTIFICATION'
const QUIT_NOTIFICATION = 'QUIT_NOTIFICATION'
const NOTIFICATION_TIMEOUT = 2000

const initialState = ''
const rootReducer = (state = initialState, action) => {
  const {type, message} = action
  console.log(type)
  switch(type) {
    case NEW_NOTIFICATION:
      return message
    break
    case QUIT_NOTIFICATION:
      return initialState
    break
  }

  return state
}

const rootEpic = (action$) => {
  const incoming = action$.ofType(NEW_NOTIFICATION)
  const outgoing = incoming.switchMap((action) => {
    return Observable.of(quitNotification())
      .delay(NOTIFICATION_TIMEOUT)
      //.takeUntil(action$.ofType(NEW_NOTIFICATION))
  });

  return outgoing;
}

export function newNotification(message) {
  return ({type: NEW_NOTIFICATION, message})
}
export function quitNotification(message) {
  return ({type: QUIT_NOTIFICATION, message});
}

export const configureStore = () => createStore(
  rootReducer,
  applyMiddleware(createEpicMiddleware(rootEpic))
)

index.js

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

const store = configureStore()

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

App.js

import React, { Component } from 'react';
import {connect} from 'react-redux'
import {newNotification} from './store.js'

class App extends Component {

  render() {
    return (
      <div className="App">
        {this.props.notificationExistance ? (<p>{this.props.notificationMessage}</p>) : ''}
        <button onClick={this.props.onNotificationRequest}>Click!</button>
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    notificationExistance : state.length > 0,
    notificationMessage : state
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    onNotificationRequest: () => dispatch(newNotification(new Date().toDateString()))
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(App)

รหัสสำคัญในการแก้ปัญหานี้เป็นเรื่องง่ายเหมือนที่คุณเห็นสิ่งเดียวที่แตกต่างจากคำตอบอื่น ๆ ก็คือฟังก์ชั่น rootEpic

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

จุดที่ 2 rootEpicของเราที่ดูแลเกี่ยวกับผลข้างเคียงตรรกะใช้รหัสประมาณ 5 บรรทัดเท่านั้นซึ่งยอดเยี่ยมมาก! รวมถึงความจริงที่ว่าเป็นการประกาศที่ค่อนข้างสวย!

จุดที่ 3 บรรทัดตามคำอธิบายของ rootEpic (ในความคิดเห็น)

const rootEpic = (action$) => {
  // sets the incoming constant as a stream 
  // of actions with  type NEW_NOTIFICATION
  const incoming = action$.ofType(NEW_NOTIFICATION)
  // Merges the "incoming" stream with the stream resulting for each call
  // This functionality is similar to flatMap (or Promise.all in some way)
  // It creates a new stream with the values of incoming and 
  // the resulting values of the stream generated by the function passed
  // but it stops the merge when incoming gets a new value SO!,
  // in result: no quitNotification action is set in the resulting stream
  // in case there is a new alert
  const outgoing = incoming.switchMap((action) => {
    // creates of observable with the value passed 
    // (a stream with only one node)
    return Observable.of(quitNotification())
      // it waits before sending the nodes 
      // from the Observable.of(...) statement
      .delay(NOTIFICATION_TIMEOUT)
  });
  // we return the resulting stream
  return outgoing;
}

ฉันหวังว่ามันจะช่วย!


คุณช่วยอธิบายว่าวิธีการ API ที่เฉพาะเจาะจงกำลังทำอะไรที่นี่เช่นswitchMap?
Dmitri Zaitsev

1
เราใช้ redux-observable ในแอป React Native ของเราบน Windows มันเป็นโซลูชันการใช้งานที่หรูหราสำหรับปัญหาที่ซับซ้อนและไม่ตรงกันสูงและมีการสนับสนุนที่ยอดเยี่ยมผ่านช่องทาง Gitter และ GitHub เลเยอร์พิเศษของความซับซ้อนนั้นคุ้มค่าก็ต่อเมื่อคุณมาถึงปัญหาที่แน่นอนซึ่งมันหมายถึงการแก้ไขแน่นอน
Matt Hargett

8

ทำไมมันยากจัง มันเป็นเพียงตรรกะของ UI ใช้การกระทำเฉพาะเพื่อตั้งค่าข้อมูลการแจ้งเตือน:

dispatch({ notificationData: { message: 'message', expire: +new Date() + 5*1000 } })

และส่วนประกอบเฉพาะเพื่อแสดง:

const Notifications = ({ notificationData }) => {
    if(notificationData.expire > this.state.currentTime) {
      return <div>{notificationData.message}</div>
    } else return null;
}

ในกรณีนี้คำถามควรเป็น "คุณจะล้างสถานะเก่าได้อย่างไร", "วิธีการแจ้งให้ทราบถึงองค์ประกอบที่มีการเปลี่ยนแปลงเวลา"

คุณสามารถใช้การกระทำ TIMEOUT บางอย่างซึ่งจัดส่งใน setTimeout จากส่วนประกอบ

อาจเป็นการดีที่จะทำความสะอาดทุกครั้งที่มีการแจ้งเตือนใหม่ปรากฏขึ้น

อย่างไรก็ตามควรมีบางsetTimeoutแห่งใช่ไหม? ทำไมไม่ทำในองค์ประกอบ

setTimeout(() => this.setState({ currentTime: +new Date()}), 
           this.props.notificationData.expire-(+new Date()) )

แรงจูงใจก็คือฟังก์ชั่น "การแจ้งเตือนจะหายไป" นั้นเป็นเรื่องเกี่ยวกับ UI ดังนั้นจึงเป็นการง่ายสำหรับการทดสอบตรรกะทางธุรกิจของคุณ

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


1
นี่ควรเป็นคำตอบที่ดีที่สุด
mmla

6

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

ให้บอกว่าคุณเป็นผู้สร้างแอคชั่นของคุณแบบนี้:

//action creator
buildAction = (actionData) => ({
    ...actionData,
    timeout: 500
})

การหมดเวลาสามารถเก็บค่าได้หลายค่าในการทำงานด้านบน

  • ตัวเลขเป็น ms - สำหรับระยะเวลาการหมดเวลาที่กำหนด
  • จริง - สำหรับช่วงเวลาหมดเวลาคงที่ (จัดการในมิดเดิลแวร์)
  • ไม่ได้กำหนด - สำหรับการจัดส่งทันที

การใช้มิดเดิลแวร์ของคุณจะเป็นดังนี้:

//timeoutMiddleware.js
const timeoutMiddleware = store => next => action => {

  //If your action doesn't have any timeout attribute, fallback to the default handler
  if(!action.timeout) {
    return next (action)
  }

  const defaultTimeoutDuration = 1000;
  const timeoutDuration = Number.isInteger(action.timeout) ? action.timeout || defaultTimeoutDuration;

//timeout here is called based on the duration defined in the action.
  setTimeout(() => {
    next (action)
  }, timeoutDuration)
}

ตอนนี้คุณสามารถกำหนดเส้นทางการกระทำทั้งหมดผ่านมิดเดิลแวร์เลเยอร์นี้โดยใช้ redux

createStore(reducer, applyMiddleware(timeoutMiddleware))

คุณสามารถหาตัวอย่างที่คล้ายกันได้ที่นี่


5

วิธีที่เหมาะสมในการทำเช่นนี้คือการใช้Redux Thunkซึ่งเป็นมิดเดิลแวร์ยอดนิยมสำหรับ Redux ตามเอกสารประกอบของ Redux Thunk:

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

ดังนั้นโดยพื้นฐานแล้วมันจะคืนค่าฟังก์ชันและคุณสามารถหน่วงเวลาการจัดส่งของคุณหรือวางไว้ในสถานะเงื่อนไข

ดังนั้นสิ่งนี้จะทำงานให้คุณ:

import ReduxThunk from 'redux-thunk';

const INCREMENT_COUNTER = 'INCREMENT_COUNTER';

function increment() {
  return {
    type: INCREMENT_COUNTER
  };
}

function incrementAsync() {
  return dispatch => {
    setTimeout(() => {
      // Yay! Can invoke sync or async actions with `dispatch`
      dispatch(increment());
    }, 5000);
  };
}


3

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

ฉันได้สร้างห้องสมุดเพื่อแก้ไขปัญหาต่างๆเช่นความฟุ่มเฟื่อยและความสามารถในการแต่งเพลงและตัวอย่างของคุณจะมีลักษณะดังนี้:

import { createTile, createSyncTile } from 'redux-tiles';
import { sleep } from 'delounce';

const notifications = createSyncTile({
  type: ['ui', 'notifications'],
  fn: ({ params }) => params.data,
  // to have only one tile for all notifications
  nesting: ({ type }) => [type],
});

const notificationsManager = createTile({
  type: ['ui', 'notificationManager'],
  fn: ({ params, dispatch, actions }) => {
    dispatch(actions.ui.notifications({ type: params.type, data: params.data }));
    await sleep(params.timeout || 5000);
    dispatch(actions.ui.notifications({ type: params.type, data: null }));
    return { closed: true };
  },
  nesting: ({ type }) => [type],
});

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

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