시간 초과로 Redux 액션을 전달하는 방법은 무엇입니까?


891

내 응용 프로그램의 알림 상태를 업데이트하는 작업이 있습니다. 일반적으로이 알림은 오류 또는 일종의 정보입니다. 그런 다음 알림 상태를 초기 상태로 되돌릴 5 초 후에 다른 작업을 전달해야하므로 알림이 없습니다. 이것의 주된 이유는 5 초 후에 알림이 자동으로 사라지는 기능을 제공하기 위해서입니다.

setTimeout다른 작업 을 사용 하고 반환 하는 것에 대해 운이 없었으며 이것이 온라인에서 수행되는 방법을 찾을 수 없습니다. 따라서 어떤 조언도 환영합니다.


30
redux-saga썽크보다 더 나은 것을 원한다면 내 대답 을 확인하는 것을 잊지 마십시오 . 늦은 답변이므로 표시하기 전에 오래 스크롤해야합니다. :) 읽을 가치가 없다는 것을 의미하지는 않습니다. 지름길은 다음과 같습니다. stackoverflow.com/a/38574266/82609
Sebastien Lorber

5
setTimeout을 수행 할 때마다 componentWillUnMount 수명주기 방법에서 clearTimeout을 사용하여 타이머를 지우는 것을 잊지 마십시오
Hemadri Dasari

2
redux-saga는 멋지지만 생성기 함수의 유형 응답을 지원하지 않는 것 같습니다. 반응에 typescript를 사용하는 경우 중요 할 수 있습니다.
Crhistian Ramirez 2014

답변:


2616

도서관이 모든 것을하는 방법을 처방해야한다고 생각하는 함정에 빠지지 마십시오 . JavaScript에서 시간 초과가있는 작업을 수행하려면을 사용해야 setTimeout합니다. Redux 작업이 다른 이유는 없습니다.

Redux 비동기적인 것들을 다루는 몇 가지 대안을 제공하지만 너무 많은 코드를 반복한다는 것을 깨달을 때만 사용해야합니다. 이 문제가 없으면 언어가 제공하는 것을 사용하여 가장 간단한 해결책을 찾으십시오.

비동기 코드 인라인 작성

이것은 가장 간단한 방법입니다. 그리고 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)

지금까지 미들웨어 또는 기타 고급 개념을 사용하지 않았습니다.

비동기 액션 생성기 추출

위의 방법은 간단한 경우에는 잘 작동하지만 몇 가지 문제가 있음을 알 수 있습니다.

  • 알림을 표시하려는 모든 위치에서이 논리를 복제해야합니다.
  • 알림에는 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.')    

이 방법은 더 단순 해 보이지만 이 방법은 권장하지 않습니다 . 우리가 싫어하는 주된 이유는 store를 singleton으로 강제 하기 때문 입니다. 따라서 서버 렌더링 을 구현하기가 매우 어렵습니다 . 서버에서 각 요청마다 고유 한 저장소를 가지도록하여 다른 사용자가 서로 다른 사전로드 된 데이터를 얻도록합니다.

단일 저장소도 테스트를 어렵게 만듭니다. 조치 작성자가 특정 모듈에서 내 보낸 특정 실제 상점을 참조하므로 조치 작성자를 테스트 할 때 더 이상 상점을 조롱 할 수 없습니다. 외부에서 상태를 재설정 할 수도 없습니다.

따라서 기술적으로 모듈에서 단일 저장소를 내보낼 수는 있지만 권장하지는 않습니다. 앱에서 서버 렌더링을 추가하지 않을 것이 아니라면이 작업을 수행하지 마십시오.

이전 버전으로 돌아 가기 :

// 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.')    

이것은 로직 복제 문제를 해결하고 경쟁 조건에서 우리를 저장합니다.

썽크 미들웨어

간단한 앱의 경우 접근 방식으로 충분합니다. 행복하다면 미들웨어에 대해 걱정하지 마십시오.

그러나 더 큰 앱에서는 주변에 특정 불편이있을 수 있습니다.

예를 들어, 우리가 통과해야하는 것은 불행한 것 같습니다 dispatch. 위와 같은 방식으로 Redux 작업을 비동기식으로 전달하는 모든 구성 요소 는 소품 으로 받아 들여 더 이상 전달할 수 있기 때문에 컨테이너와 프레젠테이션 구성 요소분리 하기가 더 까다로워 dispatch집니다. 실제로 액션 제작자가 아니기 connect()때문에 액션 제작자를 더 이상 바인딩 할 수 없습니다 showNotificationWithTimeout(). Redux 작업을 반환하지 않습니다.

또한 어떤 함수가 동기 작업 작성자와 같은 showNotification()비동기 도우미 인지 기억하는 것은 어색 할 수 있습니다 showNotificationWithTimeout(). 서로 다르게 사용해야하며 서로 실수하지 않도록주의해야합니다.

이것은 도우미 기능에 이러한 패턴을 제공하는 방법을 찾고, dispatchRedux가 이러한 비동기식 액션 제작자를 완전히 다른 기능이 아닌 일반 액션 제작자의 특별한 경우로 "볼 수 있도록 " 하는 동기를 부여했습니다 .

여전히 우리와 함께 있고 앱에서 문제로 인식되면 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 액션 제작자로 선언 할 수 있습니다 .

// 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)

우리는 단지 원하는 내부 기능을 얻을 수있는 비동기 작업 창조자를 호출 dispatch한 다음 우리가 통과 dispatch.

그러나 이것은 원래 버전보다 훨씬 어색합니다! 우리는 왜 그런 식으로 갔습니까?

내가 전에 한 말 때문에 Redux Thunk 미들웨어가 사용 가능한 경우 조치 오브젝트 대신 함수를 디스패치하려고 시도 할 때마다 미들웨어는 dispatch메소드 자체를 첫 번째 인수로 사용하여 해당 함수를 호출합니다 .

그래서 우리는 대신 이것을 할 수 있습니다 :

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

마지막으로, 비동기 액션 (실제로 일련의 액션)을 디스패치하는 것은 단일 액션을 컴포넌트에 동 기적으로 디스패치하는 것과 다르지 않습니다. 구성 요소가 동 기적으로 또는 비동기 적으로 발생하는지 여부를 신경 쓰지 않아야하기 때문에 좋습니다. 우리는 그것을 추상화했습니다.

우리가 "가르쳐"이후 돌아 오는이 (우리가 그들에게 전화 등의 "특별한"액션 제작자를 인식하는 것을 알 수 썽크 우리는 일반 액션 제작자을 사용 곳 액션 제작자), 우리는 지금 어떤 장소에서 사용할 수 있습니다. 예를 들어 다음과 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)

뭉크의 읽기 상태

일반적으로 감속기는 다음 상태를 결정하기위한 비즈니스 로직을 포함합니다. 그러나 감속기는 작업이 발송 된 후에 만 ​​시작됩니다. 썽크 액션 생성자에 부작용 (예 : API 호출)이 있고 어떤 조건에서이를 방지하려면 어떻게해야합니까?

썽크 미들웨어를 사용하지 않고 구성 요소 내부 에서이 검사를 수행하면됩니다.

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

그러나 액션 제작자를 추출하는 요점은 여러 구성 요소에서이 반복적 인 논리를 중앙 집중화하는 것이 었습니다. 다행히 Redux Thunk는 Redux 저장소의 현재 상태 를 읽을 수있는 방법을 제공합니다 . 뿐만 아니라 썽크 액션 생성자에서 반환하는 함수에 대한 두 번째 인수로 dispatch도 전달 getState됩니다. 이를 통해 썽 크는 상점의 현재 상태를 읽을 수 있습니다.

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()조건부로 다른 조치를 디스패치 하는 데만 사용 하는 경우 비즈니스 로직을 리듀서에 대신 사용하십시오.

다음 단계

썽크의 작동 방식에 대한 기본적인 이해가 있으므로 이를 사용하는 Redux 비동기 예제 를 확인하십시오 .

썽크가 약속을 반환하는 많은 예를 찾을 수 있습니다. 이것은 필수는 아니지만 매우 편리합니다. Redux는 썽크에서 반환하는 것을 신경 쓰지 않지만에서 반환 값을 제공합니다 dispatch(). 그렇기 때문에 썽크에서 약속을 반환하고을 호출하여 완료 될 때까지 기다릴 수 있습니다 dispatch(someThunkReturningPromise()).then(...).

복잡한 썽크 액션 제작자를 여러 개의 작은 썽크 액션 제작자로 나눌 수도 있습니다. dispatch재귀 패턴을 적용 할 수 있도록 썽크에 의해 제공 방법은, 그 자체 썽크 받아 들일 수 있습니다. 다시 말하지만, 비동기 제어 흐름을 구현할 수 있기 때문에 Promises와 가장 잘 작동합니다.

일부 앱의 경우 비동기 제어 흐름 요구 사항이 너무 복잡하여 썽 크로 표현할 수없는 상황에 처할 수 있습니다. 예를 들어, 실패한 요청을 재 시도하거나 토큰을 사용한 재 인증 흐름 또는 단계별 온 보딩은 너무 자세하고 오류가 발생하기 쉽습니다. 이 경우 Redux Saga 또는 Redux Loop 와 같은 고급 비동기 제어 흐름 솔루션을 살펴볼 수 있습니다 . 그것들을 평가하고, 귀하의 필요와 관련된 예를 비교하고, 가장 좋아하는 것을 선택하십시오.

마지막으로 꼭 필요한 것이 없다면 썽크를 포함하여 아무 것도 사용하지 마십시오. 요구 사항에 따라 솔루션은 다음과 같이 단순 해 보일 수 있습니다.

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

왜 이런 짓을하는지 모른다면 땀을 흘리지 마십시오.


27
비동기 작업은 일반적인 문제에 대한 간단하고 우아한 솔루션처럼 보입니다. 미들웨어가 없어도 redux로 구워지지 않은 이유는 무엇입니까? 이 대답은 훨씬 간결 할 수 있습니다.
Phil Mander

83
@PhilMander github.com/raisemarketplace/redux-loop 또는 github.com/yelouafi/redux-saga 와 같은 많은 대안 패턴이 있기 때문에 (더 이상은 아니지만) 우아합니다. Redux는 저수준 도구입니다. 원하는 수퍼 셋을 만들어 별도로 배포 할 수 있습니다.
Dan Abramov

16
이것을 설명 할 수 있습니까? * 비즈니스 로직을 리듀서에 넣는 것을 고려하십시오. * 이는 조치를 디스패치 한 다음 리듀서에서 상태에 따라 디스패치 할 추가 조치를 결정해야합니까? 내 질문은, 그런 다음 감속기에 직접 다른 행동을 파견하고, 그렇지 않은 경우 어디에서 파견해야합니까?
froginvasion

25
이 문장은 동기식 경우에만 적용됩니다. 예를 들어 글을 쓰는 경우 현재 상태 에 따라 감속기의 동작을 무시하고 무시하도록 선택 if (cond) dispatch({ type: 'A' }) else dispatch({ type: 'B' })해야합니다 . dispatch({ type: 'C', something: cond })action.something
Dan Abramov

29
@DanAbramov "이 문제가 없다면 언어가 제공하는 것을 사용하고 가장 간단한 해결책을 찾으십시오." 그 후에야 누가 그것을 썼는지 깨달았습니다!
Matt Lacey

189

Redux-Saga 사용

Dan Abramov가 말했듯이, 비동기 코드에 대한 고급 제어를 원한다면 redux-saga를 살펴보십시오 .

이 답변은 간단한 예입니다. redux-saga가 애플리케이션에 유용한 이유에 대한 자세한 설명을 원하면 이 다른 답변을 확인하십시오 .

일반적인 아이디어는 Redux-saga가 ES6 생성기 인터프리터를 제공하여 동기 코드처럼 보이는 비동기 코드를 쉽게 작성할 수 있도록하는 것입니다 (이러한 이유로 Redux-saga에서 무한 while 루프가 자주 발생합니다). 어떻게 든 Redux-saga는 Javascript 내부에서 직접 자체 언어를 구축하고 있습니다. Redux-saga는 처음에는 배우기가 어려울 수 있습니다. 생성기에 대한 기본적인 이해가 필요하지만 Redux-saga에서 제공하는 언어도 이해하기 때문입니다.

여기 redux-saga 위에 구축 한 알림 시스템을 여기에 설명하려고합니다. 이 예제는 현재 프로덕션에서 실행됩니다.

고급 알림 시스템 사양

  • 알림을 표시하도록 요청할 수 있습니다
  • 숨기기 알림을 요청할 수 있습니다
  • 알림은 4 초 이상 표시되지 않아야합니다.
  • 여러 알림을 동시에 표시 할 수 있습니다
  • 동시에 3 개 이하의 알림을 표시 할 수 없습니다
  • 3 개의 표시된 알림이 이미있는 동안 알림이 요청 된 경우 알림을 대기열에 넣거나 연기합니다.

결과

내 프로덕션 앱 Stample.co의 스크린 샷

토스트

암호

여기에 알림 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 개의 알림 만 표시되며 첫 번째 알림이 사라지면 네 번째 알림이 조금 나중에 나타납니다.

TOAST_DISPLAY_REQUESTEDJSX에서 발송하는 것은 특별히 권장하지 않습니다 . 기존 앱 이벤트를 수신하는 다른 사가를 추가 한 다음TOAST_DISPLAY_REQUESTED 하는 알림을 추가 한 다음 알림을 트리거하는 구성 요소를 알림 시스템에 밀접하게 연결하지 않아도됩니다.

결론

내 코드는 완벽하지는 않지만 몇 달 동안 0 개의 버그로 프로덕션 환경에서 실행됩니다. Redux-saga와 제너레이터는 처음에는 약간 어렵지만 일단 이해하면 이러한 종류의 시스템을 쉽게 구축 할 수 있습니다.

다음과 같이보다 복잡한 규칙을 구현하는 것은 매우 쉽습니다.

  • 너무 많은 알림이 "대기 중"인 경우 각 알림에 대해 표시 시간을 줄이면 대기열 크기가 더 빨리 줄어들 수 있습니다.
  • 창 크기 변경을 감지하고 이에 따라 표시되는 최대 알림 수를 변경합니다 (예 : 데스크톱 = 3, 전화 세로 = 2, 전화 가로 = 1).

솔직히 이런 종류의 것들을 썽크와 올바르게 구현하는 것이 행운입니다.

redux-saga와 매우 유사한 redux-observable 을 사용 하여 정확히 같은 종류의 작업을 수행 할 수 있습니다 . 거의 동일하며 발전기와 RxJS 사이의 맛의 문제입니다.


18
Saga 부작용 라이브러리를 이와 같은 비즈니스 로직에 사용하는 것에 더 동의 할 수 없기 때문에 질문을 받았을 때 귀하의 답변이 일찍 나오기를 바랍니다. 리듀서 및 액션 크리에이터는 상태 전환을위한 것입니다. 워크 플로우는 상태 전이 기능과 다릅니다. 워크 플로는 전환 단계를 거치지 만 전환 자체는 아닙니다. Redux + React 자체에는 이것이 부족합니다. 이것이 바로 Redux Saga가 유용한 이유입니다.
Atticus

4
감사합니다, 나는 이러한 이유로 REDUX-사가 인기를 만들기 위해 최선을 다하려고합니다 :) 너무 적은 사람들은 REDUX-사가 그냥 썽크를 대체 현재 생각하고 REDUX-사가 복잡하고 분리 된 워크 플로우를 가능하게 표시되지 않습니다
세바스티앙 Lorber을

1
바로 그거죠. 동작 및 감속기는 모두 상태 시스템의 일부입니다. 때로는 복잡한 워크 플로의 경우 상태 머신 자체에 직접 포함되지 않은 상태 머신을 조정하기 위해 다른 것이 필요합니다!
Atticus

2
작업 : 페이로드 / 이벤트를 전환 상태로 전환합니다. 감속기 : 상태 전환 기능. 구성 요소 : 상태를 반영하는 사용자 인터페이스. 그러나 한 가지 중요한 부분이 누락되었습니다. 다음에 수행 할 전환을 결정하는 자체 논리가있는 많은 전환의 프로세스를 어떻게 관리합니까? Redux Saga!
Atticus

2
@mrbrdo 내 대답을주의 깊게 읽으면 알림 시간 초과가 실제로 처리됩니다 yield call(delay,timeoutValue);. 동일한 API는 아니지만 동일한 효과가 있습니다.
Sebastien Lorber

25

샘플 프로젝트가있는 저장소

현재 네 가지 샘플 프로젝트가 있습니다.

  1. 비동기 코드 인라인 작성
  2. 비동기 액션 생성기 추출
  3. Redux Thunk 사용
  4. Redux Saga 사용

허용되는 답변은 훌륭합니다.

그러나 빠진 것이 있습니다.

  1. 실행할 수있는 샘플 프로젝트가없고 일부 코드 스 니펫 만 있습니다.
  2. 다음과 같은 다른 대안에 대한 샘플 코드는 없습니다.
    1. 레독 사가

그래서 나는 누락 된 것들을 추가하기 위해 Hello Async 저장소를 만들었습니다 .

  1. 실행 가능한 프로젝트. 수정없이 다운로드하여 실행할 수 있습니다.
  2. 더 많은 대안을위한 샘플 코드를 제공하십시오.

레독 사가

허용 된 답변은 이미 비동기 코드 인라인, 비동기 동작 생성기 및 Redux 썽크에 대한 샘플 코드 스 니펫을 제공합니다. 완벽을 기하기 위해 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 Generator에 의존합니다.

위의 코드 스 니펫이 모든 질문에 대답하지 않으면 실행 가능한 프로젝트를 참조하십시오 .


23

redux-thunk 로이 작업을 수행 할 수 있습니다 . redux 문서 에는 setTimeout과 같은 비동기 작업에 대한 가이드 가 있습니다 .


미들웨어를 사용할 때 간단한 후속 조치 질문 applyMiddleware(ReduxPromise, thunk)(createStore)은 썽크가 작동하지 않는 것처럼 여러 미들웨어를 추가하는 방법입니다 (쉼표로 구분?).
Ilja

1
@Ilja이 작동합니다 :const store = createStore(reducer, applyMiddleware([ReduxPromise, thunk]));
geniuscarrier

22

SAM 패턴도 살펴 보는 것이 좋습니다 .

SAM 패턴은 모델이 업데이트되면 "5 초 후에 알림이 자동으로 사라짐"과 같은 (자동) 작업이 트리거되는 "다음 작업 조건 자"를 포함하도록 권장합니다 (SAM 모델 ~ 감속기 상태 + 저장).

이 패턴은 모델의 "제어 상태"가 다음 조치 술어에 의해 조치가 사용 가능하고 /하거나 자동으로 실행되는 것을 "제어"하므로 시퀀싱 조치 및 모델 돌연변이를 한 번에 하나씩 옹호합니다. 조치를 처리하기 전에 시스템 상태를 예측할 수 없으므로 일반적으로 다음 예상 조치가 허용 / 가능한지 여부를 예측할 수 없습니다.

예를 들어 코드는

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

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

hideNotification 작업을 전달할 수 있다는 사실은 "showNotication : true"값을 성공적으로 수락하는 모델에 따라 다르므로 SAM에서는 허용되지 않습니다. 모델의 다른 부분이 모델을 수락하지 못하게 할 수 있으므로 hideNotification 조치를 트리거 할 이유가 없습니다.

상점 업데이트 후 모델의 새로운 제어 상태를 알 수있는 경우 적절한 다음 조치 술어를 구현하는 것이 좋습니다. 그것은 당신이 찾고있는 행동을 구현하는 가장 안전한 방법입니다.

원하는 경우 Gitter에서 우리와 함께 할 수 있습니다. 여기에 SAM 시작 안내서도 있습니다 .


나는 지금까지 표면을 긁어 냈지만 이미 SAM 패턴에 감격했습니다. V = S( vm( M.present( A(data) ) ), nap(M))그냥 아름답습니다. 당신의 생각과 경험을 공유해 주셔서 감사합니다. 더 깊이 파고 들게

@ftor, 감사합니다! 처음 썼을 때도 같은 느낌이 들었습니다. 나는 거의 1 년 동안 프로덕션 환경에서 SAM을 사용해 왔으며, SAM을 구현하기 위해 라이브러리가 필요하다고 느꼈던 시간을 생각할 수 없었습니다 (vdom도 사용할 수 있지만 볼 수 있음). 한 줄의 코드로 끝났습니다! SAM은 동형 코드를 생성하므로 비동기 호출을 처리하는 방법에 대한 모호성이 없습니다 ... 그래도 시간이 어디인지 모르겠습니다. 어떻게해야합니까?
메타 프로그래머

SAM은 진정한 소프트웨어 엔지니어링 패턴입니다 (Alexa SDK와 함께 제작했습니다). TLA +를 기반으로하며 모든 개발자에게 놀라운 작업의 힘을 가져 오려고합니다. SAM은 수십 년 동안 모든 사람들이 사용한 거의 세 가지 근사치를 수정합니다.-작업이 애플리케이션 상태를 조작 할 수 있습니다.-할당은 돌연변이와 동일합니다.-프로그래밍 단계가 무엇인지에 대한 정확한 정의가 없습니다 (예 : a = b * ca 단계) , 1 / 읽기 b, c 2 / 컴퓨 트 b * c, 3 /는 결과에 세 가지 다른 단계를 할당합니까?
메타 프로그래머

20

다양한 인기있는 접근 방식 (액션 제작자, 썽크, 사가, 에픽, 효과, 사용자 정의 미들웨어)을 시도한 후에도 여전히 개선의 여지가 있다고 생각 하여이 블로그 기사에서 여행을 문서화 했습니다. React / Redux 애플리케이션?

여기서 논의한 것처럼, 나는 다양한 접근법을 대조하고 비교하려고 노력했다. 결국 새 도서관을 소개하게되었습니다 epics, sagas, custom 미들웨어로부터 영감을 redux-logic .

비동기 IO를 수행하는 방법을 제공 할뿐만 아니라 유효성 검증, 검증, 권한 부여를위한 조치를 인터셉트 할 수 있습니다.

일부 공통 기능은 단순히 수신 거부, 제한, 취소 및 최신 요청 (takeLatest)의 응답 만 사용하여 선언 할 수 있습니다. redux-logic은 코드를 감싸서이 기능을 제공합니다.

따라서 핵심 비즈니스 로직을 원하는대로 구현할 수 있습니다. 원치 않는 한 관측 가능 장치 나 생성기를 사용할 필요가 없습니다. 함수 및 콜백, 약속, 비동기 함수 (비동기 / 대기) 등을 사용하십시오.

간단한 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);
  }
});
    

내 저장소에 고급 알림 예제가 있는데, Sebastian Lorber가 N 항목으로 디스플레이를 제한하고 대기중인 항목을 통해 회전 할 수있는 위치에 대해 설명 한 것과 비슷합니다. redux-logic 알림 예제

다양한 redux-logic jsfiddle 라이브 예제와 전체 예제가 있습니다. 있습니다. 나는 문서와 예제를 계속 연구하고 있습니다.

귀하의 의견을 듣고 싶습니다.


나는 당신의 도서관이 마음에 들지 않지만 당신의 기사를 좋아합니다! 잘 했어! 다른 사람의 시간을 절약하기 위해 충분한 작업을 수행했습니다.
Tyler Long

2
redux-logic에 대한 샘플 프로젝트를 만들었습니다 : github.com/tylerlong/hello-async/tree/master/redux-logic 나는 그것이 잘 설계된 소프트웨어라고 생각하며 다른 것에 비해 큰 단점을 보지 못합니다. 대안.
Tyler Long

9

나는이 질문이 조금 오래되었다는 것을 이해하지만 redux-observable 일명을 사용하는 다른 솔루션을 소개 할 것 입니다. 서사시.

공식 문서 인용 :

redux-observable은 무엇입니까?

Redux 용 RxJS 5 기반 미들웨어. 비동기 작업을 작성 및 취소하여 부작용 등을 만듭니다.

에픽은 redux-observable의 핵심 프리미티브입니다.

액션 스트림을 취해 액션 스트림을 돌려주는 함수입니다. 행동, 행동.

간단히 말해서 Stream을 통해 작업을 수신 한 다음 새 작업 스트림을 반환하는 함수를 만들 수 있습니다 (시간 초과, 지연, 간격 및 요청과 같은 일반적인 부작용 사용).

코드를 게시 한 다음 조금 더 설명하겠습니다

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. sagas와 마찬가지로 액션 스트림을 수신하고 액션 스트림을 반환하는 최상위 기능을 얻기 위해 epics를 결합해야 미들웨어 팩토리 createEpicMiddleware 와 함께 사용할 수 있습니다 . 우리의 경우 rootEpic 만 필요하므로 아무것도 결합하지 않아도 사실을 아는 것이 좋습니다.

포인트 2. 우리의 rootEpic 부작용 논리를 처리하는 은 약 5 줄의 코드 만 사용합니다. 거의 선언적이라는 사실을 포함!

포인트 3. 라인 별 루트

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
Windows의 React Native 앱에서 redux-observable을 사용하고 있습니다. 복잡하고 비동기적인 문제에 대한 우아한 구현 솔루션이며 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;
}

이 경우 질문은 "이전 상태를 어떻게 정리합니까?", "시간이 변경되었음을 구성 요소에 알리는 방법"이어야합니다.

컴포넌트에서 setTimeout에 전달되는 TIMEOUT 조치를 구현할 수 있습니다.

새 알림이 표시 될 때마다 청소하는 것이 좋습니다.

어쨌든 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 단위의 숫자-특정 시간 초과 기간
  • true-일정한 시간 초과 기간 동안. (미들웨어로 처리)
  • undefined-즉시 발송

미들웨어 구현은 다음과 같습니다.

//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 미들웨어를 사용하면 조치 대신 함수를 리턴하는 조치 작성자를 작성할 수 있습니다. 덩어리는 조치의 디스패치를 ​​지연 시키거나 특정 조건이 충족되는 경우에만 디스패치하는 데 사용될 수 있습니다. 내부 함수는 상점 메소드를 수신합니다. 디스패치 및 getState를 매개 변수로 "

따라서 기본적으로 함수를 반환하므로 디스패치를 ​​지연 시키거나 조건 상태로 만들 수 있습니다.

따라서 이와 같은 일이 당신을 위해 일할 것입니다.

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);
  };
}

4

간단하다. 사용 트림 - REDUX 이처럼 패키지와 쓰기componentDidMount 또는 다른 장소에서 그것을 죽일 componentWillUnmount.

componentDidMount() {
  this.tm = setTimeout(function() {
    setStore({ age: 20 });
  }, 3000);
}

componentWillUnmount() {
  clearTimeout(this.tm);
}

3

Redux 자체는 매우 장황한 라이브러리이므로 이러한 것들을 위해서는 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],
});

따라서 비동기 작업 내에 알림을 표시하기위한 동기화 작업을 구성하여 백그라운드에 대한 정보를 요청하거나 나중에 알림이 수동으로 닫혔는지 확인할 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.