Redux에서 비동기 흐름을 위해 미들웨어가 필요한 이유는 무엇입니까?


686

문서에 따르면 "미들웨어없이 Redux 저장소는 동기식 데이터 흐름 만 지원합니다 . " 나는 이것이 왜 그런지 이해하지 못한다. 컨테이너 구성 요소가 비동기 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. 앱에 업데이트 중임을 알리는 작업을 전달한 다음 비동기 호출을 수행합니다. 호출이 완료되면 제공된 값이 다른 조치의 페이로드로 전달됩니다.

이 접근 방식에 어떤 문제가 있습니까? 설명서에서 제안하는 것처럼 왜 Redux Thunk 또는 Redux Promise를 사용하고 싶습니까?

편집 : 나는 Redux 저장소에서 단서를 찾아서 과거에는 Action Creators가 순수한 기능이어야한다는 것을 알았습니다. 예를 들어 다음 은 비동기 데이터 흐름에 대한 더 나은 설명을 제공하려는 사용자입니다.

액션 생성자 자체는 여전히 순수한 함수이지만 반환하는 썽크 함수는 필요하지 않으며 비동기 호출을 수행 할 수 있습니다

액션 제작자는 더 이상 순수 할 필요가 없습니다. 따라서 과거에는 썽크 / 약속 미들웨어가 필요했지만 더 이상 그렇지 않은 것 같습니다.


53
액션 제작자는 순수한 기능 일 필요가 없었습니다. 변경 사항이 아닌 문서에서 실수였습니다.
Dan Abramov

1
테스트 가능성을 위해 @DanAbramov 그러나 그것은 좋은 사전 예방 적 일 수 있습니다. Redux-saga는 이것을 허용합니다 : stackoverflow.com/a/34623840/82609
Sebastien Lorber

답변:


702

이 접근 방식에 어떤 문제가 있습니까? 설명서에서 제안하는 것처럼 왜 Redux Thunk 또는 Redux Promise를 사용하고 싶습니까?

이 방법에는 아무런 문제가 없습니다. 동일한 작업을 수행하는 다른 구성 요소가 있거나 일부 작업을 디 바운싱하거나 자동 증분 ID와 같은 로컬 상태를 작업 작성자와 가깝게 유지하기 때문에 대규모 응용 프로그램에서는 불편합니다. 유지 보수 관점은 조치 작성자를 별도의 기능으로 추출합니다.

보다 자세한 연습은 “시간 초과로 Redux 작업을 발송하는 방법”에 대한 나의 답변을 읽을 수 있습니다 .

Redux Thunk 또는 Redux Promise와 같은 미들웨어는 썽크 또는 약속을 발송하기위한 "구문 설탕"을 제공하지만 이를 사용할 필요는 없습니다 .

따라서 미들웨어가 없으면 액션 제작자가 다음과 같이 보일 수 있습니다.

// 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짧은 구문 등으로 그러한 액션 생성자를 바인딩 하는 데 사용할 수 있습니다 . 구성 요소를 변경하지 않고 반면, 이전의 명시 적 접근 방식을 사용하면 구성 요소 는 특정 호출이 비동기 임을 정확히 알고 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를 사용하면 액션 제작자는 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하려면 호출 코드를 전혀 수정하지 않고 썽크에 전달 된 두 번째 인수를 사용할 수 있습니다 .

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 입니다. Redux Saga 를 사용하면 수행되는 작업을 수행하고 작업을 출력하기 전에 요청을 변환하거나 수행하는 장기 실행 데몬 ( "sagas")을 정의 할 수 있습니다. 이것은 로직을 액션 제작자에서 사 가스로 옮깁니다. 그것을 확인하고 나중에 가장 적합한 것을 선택하십시오.

나는 Redux 저장소에서 단서를 찾아서 과거에는 Action Creators가 순수한 기능이어야한다는 것을 알았습니다.

이것은 올바르지 않습니다. 문서는 이것을 말했지만 문서는 잘못되었습니다.
액션 제작자는 순수한 기능 일 필요가 없었습니다.
이를 반영하기 위해 문서를 수정했습니다.


57
Dan이 생각하는 짧은 방법은 다음과 같습니다. 미들웨어는 중앙 집중식 접근 방식이므로 구성 요소를보다 단순하고 일반화하고 한 곳에서 데이터 흐름을 제어 할 수 있습니다. 당신이 큰 응용 프로그램을 유지하면 그것을 즐길 것입니다 =)
Sergey Lapin

3
@asdfasdfads 왜 작동하지 않는지 모르겠습니다. 정확히 동일하게 작동합니다. 행동을 취한 alert후에 놓으십시오 dispatch().
댄 아브라모프

9
첫 번째 코드 예제의 두 번째 줄 : loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch. 왜 파견을 통과해야합니까? 컨벤션에 따라 단일 글로벌 상점 만있는 경우 직접 참조하고 직접 store.dispatch필요할 때마다 수행 해야하는 이유는 loadData무엇입니까?
소렌 데 부아

10
@ SørenDebois 앱이 클라이언트 측 인 경우 작동합니다. 서버에서 렌더링되는 경우 store모든 요청에 ​​대해 서로 다른 인스턴스 가 있어야 미리 정의 할 수 없습니다.
Dan Abramov

3
이 답변에는 14 줄로 구성된 redux-thunk의 소스 코드보다 9.92 배 더 많은 139 줄이 있음을 지적하고 싶습니다. github.com/gaearon/redux-thunk/blob/master/src/index.js
Guy

447

당신은하지 않습니다.

그러나 ... 당신은 redux-saga를 사용해야합니다 :)

Dan Abramov의 대답은 옳습니다. redux-thunk그러나 나는 아주 비슷하지만 더 강력한 redux-saga 에 대해 조금 더 이야기 할 것 입니다.

명령형 VS 선언

  • DOM : jQuery는 필수적입니다 ./ 반응은 선언적입니다.
  • Monads : IO는 필수입니다 / 무료는 선언적입니다
  • 환원 효과 : redux-thunk명령 적 / redux-saga선언적

IO 모나드 또는 약속과 같은 손에 썽크가 있으면, 일단 실행 한 후에는 무엇을하는지 쉽게 알 수 없습니다. 썽크를 테스트하는 유일한 방법은 펑크를 실행하고 디스패처 (또는 더 많은 것들과 상호 작용하는 경우 외부 세계 전체를 조롱하는 것)입니다.

모의를 사용하는 경우 기능 프로그래밍을 수행하지 않는 것입니다.

부작용의 렌즈를 통해 볼 때 모의는 코드가 불완전하다는 플래그이며 함수형 프로그래머의 눈에는 문제가 있음을 증명합니다. 빙산이 손상되지 않았는지 확인하기 위해 라이브러리를 다운로드하는 대신 주변을 항해해야합니다. 하드 코어 TDD / 자바 남자가 Clojure에서 조롱하는 방법을 물었습니다. 대답은 보통 그렇지 않습니다. 우리는 일반적으로 코드를 리팩토링하는 데 필요한 신호로 본다.

출처

사가 redux-saga )는 선언적이며 Free 모나드 또는 React 구성 요소와 마찬가지로 모의 실험없이 훨씬 쉽게 테스트 할 수 있습니다.

기사를 참조하십시오 :

현대 FP에서는 프로그램을 작성해서는 안됩니다. 프로그램에 대한 설명을 작성해야합니다. 그러면 프로그램을 설명하고 변환하고 해석 할 수 있습니다.

(실제로 Redux-saga는 하이브리드와 같습니다. 흐름은 필수적이지만 효과는 선언적입니다)

혼란 : 액션 / 이벤트 / 명령 ...

CQRS / EventSourcing 및 Flux / Redux와 같은 일부 백엔드 개념이 어떻게 관련 될 수 있는지에 대한 프론트 엔드 세계에는 많은 혼란이 있습니다. Flux에서는 종종 명령 코드 ( LOAD_USER)와 이벤트 ( USER_LOADED). 이벤트 소싱과 마찬가지로 이벤트 만 전달해야한다고 생각합니다.

실제로 사가 사용

사용자 프로필에 대한 링크가있는 앱을 상상해보십시오. 각 미들웨어로이를 처리하는 관용적 방법은 다음과 같습니다.

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및 components dispatch) 를 유지하는 것이 여전히 유용합니다 . 앞으로 액션 검증 (어설 션 / 플로우 / 유형)을 추가하는 데 도움이 될 수 있습니다.

효과가 선언적이므로 코드를 훨씬 더 테스트 할 수 있습니다.

더 이상 rpc와 같은 호출을 트리거 할 필요가 없습니다 actions.loadUser(). UI는 HAPPENED를 전달하기 만하면됩니다. 우리는 사건을 발동 하고 (항상 시제로) 더 이상 행동하지 않습니다. 즉, 분리 된 "덕" 또는 바운드 컨텍스트를 만들 수 있으며 saga가 이러한 모듈 식 구성 요소 사이의 연결 지점 역할을 할 수 있습니다.

즉, 발생한 일과 결과로 발생하는 일 사이에 해당 변환 계층을 더 이상 포함 할 필요가 없으므로 뷰를보다 쉽게 ​​관리 할 수 ​​있습니다.

예를 들어 무한 스크롤보기를 상상해보십시오. CONTAINER_SCROLLED로 이어질 수 NEXT_PAGE_LOADED있지만 실제로 다른 페이지를로드 해야하는지 여부를 결정하는 것은 스크롤 가능한 컨테이너의 책임입니까? 그런 다음 마지막 페이지가 성공적으로로드되었는지 또는 이미로드하려는 페이지가 있는지 또는 더 이상로드 할 항목이 없는지 여부와 같은 더 복잡한 내용을 알고 있어야합니까? 나는 그렇게 생각하지 않습니다 : 재사용 성을 극대화하기 위해 스크롤 가능한 컨테이너는 스크롤되었다는 것을 설명해야합니다. 페이지로드는 해당 스크롤의 "비즈니스 효과"입니다

일부는 생성자가 로컬 변수를 사용하여 redux 저장소 외부의 상태를 본질적으로 숨길 수 있다고 주장 할 수 있지만 타이머 등을 시작하여 썽크 내부의 복잡한 것을 조정하기 시작하면 어쨌든 동일한 문제가 발생합니다. 그리고 select이제 Redux 스토어에서 상태를 가져올 수 있는 효과가 있습니다.

Sagas는 시간 여행이 가능하며 현재 진행중인 복잡한 흐름 로깅 및 개발 도구도 사용할 수 있습니다. 다음은 이미 구현 된 간단한 비동기 흐름 로깅입니다.

사가 흐름 로깅

디커플링

Sagas는 redux 썽크 만 교체하지 않습니다. 백엔드 / 분산 시스템 / 이벤트 소싱에서 나옵니다.

sagas가 redux 썽크를 더 나은 테스트 가능성으로 대체하기 위해 여기에 있다는 것은 매우 일반적인 오해입니다. 실제로 이것은 redux-saga의 구현 세부 사항입니다. 선언적 효과를 사용하는 것이 테스트 가능성을 위해 썽크보다 낫지 만 사가 패턴은 명령형 또는 선언적 코드 위에 구현 될 수 있습니다.

우선 saga는 장기 실행 트랜잭션 (최종 일관성)과 서로 다른 경계 컨텍스트 (도메인 기반 디자인 전문 용어)를 통한 트랜잭션을 조정할 수있는 소프트웨어입니다.

프론트 엔드 세계에서이를 단순화하려면 widget1과 widget2가 있다고 가정하십시오. widget1의 일부 단추를 클릭하면 widget2에 영향을줍니다. widget1은 위젯 2 개를 함께 결합하는 대신 (즉, widget1은 widget2를 대상으로하는 조치를 전달 함) 단추를 클릭 한 것만 전달합니다. 그런 다음 saga는이 단추 클릭을 청취하고 widget2가 인식하는 새 이벤트를 표시하여 widget2를 업데이트하십시오.

따라서 간단한 앱에는 필요하지 않은 간접 수준이 추가되지만 복잡한 애플리케이션을보다 쉽게 ​​확장 할 수 있습니다. 이제 widget1 및 widget2를 다른 npm 저장소에 공개하여 글로벌 조치 레지스트리를 공유하지 않고도 서로에 대해 알 필요가 없습니다. 2 개의 위젯은 이제 별도로 존재할 수있는 제한된 컨텍스트입니다. 서로 일관 될 필요가 없으며 다른 앱에서도 재사용 할 수 있습니다. saga는 비즈니스에 의미있는 방식으로 조정하는 두 위젯 간의 연결점입니다.

Redux 앱을 구성하는 방법에 대한 멋진 기사로, 디커플링 이유로 Redux-saga를 사용할 수 있습니다.

구체적인 사용 사례 : 알림 시스템

구성 요소가 인앱 알림 표시를 트리거 할 수 있기를 원합니다. 그러나 구성 요소가 자체 비즈니스 규칙 (동시에 표시되는 최대 3 개의 알림, 알림 대기열, 4 초 표시 시간 등)이있는 알림 시스템에 고도로 결합되기를 원하지 않습니다.

JSX 구성 요소가 알림을 표시하거나 숨길 시간을 결정하고 싶지 않습니다. 나는 단지 알림을 요청하는 능력을 부여하고, 사가 안에 복잡한 규칙을 남겨 둡니다. 이런 종류의 물건은 썽크 나 약속으로 구현하기가 매우 어렵습니다.

알림

여기 에 saga로 어떻게 할 수 있는지 설명 했습니다.

왜 사가라고 불리는가?

사가라는 용어는 백엔드 세계에서 비롯됩니다. 나는 처음에 야사 인 (Redux-saga의 저자)을 긴 토론 에서 그 용어에 대해 소개했다 .

처음에는이 용어가 논문 과 함께 소개되었는데 , 사가 패턴은 분산 트랜잭션에서 최종 일관성을 처리하는 데 사용되었지만 백엔드 개발자는이를 "프로세스 관리자"도 다루도록 광범위하게 정의했습니다. 패턴 (어떻게도 원래의 사가 패턴은 프로세스 관리자의 특수한 형태입니다).

오늘날, "사가"라는 용어는 두 가지 다른 것을 설명 할 수 있으므로 혼동됩니다. redux-saga에서 사용되므로 분산 트랜잭션을 처리하는 방법이 아니라 앱의 작업을 조정하는 방법을 설명합니다. redux-saga라고도 할 수 있습니다 redux-process-manager.

또한보십시오:

대안

생성기를 사용하는 아이디어는 마음에 들지 않지만 saga 패턴 및 디커플링 특성에 관심이있는 경우 이름 을 사용 하여 정확히 동일한 패턴을 설명하지만 RxJS 를 사용하는 redux-observable 을 사용하여 epic동일한 결과를 얻을 수도 있습니다. 이미 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 호출만으로는 가치가 없습니다.
  • 가장 간단한 경우 프로젝트에서 썽크를 제거하지 마십시오.
  • yield put(someActionThunk)말이 되더라도 주저하지 마십시오 .

Redux-saga (또는 Redux-observable) 사용에 두려워하지만 디커플링 패턴이 필요한 경우 redux-dispatch-subscribe를 확인하십시오 : 리스너에서 디스패치를 ​​듣고 새 디스패치를 ​​트리거 할 수 있습니다.

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 늦게 죄송합니다. 당신이 파견 할 때 ADD_ITEM당신이 작업을 파견하기 때문에, 그것은 목적이 상점에 영향을하는 것이 필수적이다 : 당신이 액션이 뭔가를 기대합니다. 선언적으로 이벤트 소싱의 철학을 받아들입니다. 응용 프로그램에서 변경 사항을 트리거하는 작업을 전달하지 않지만 응용 프로그램에서 발생한 일을 설명하기 위해 과거 이벤트를 전달합니다. 이벤트의 디스패치는 애플리케이션 상태가 변경되었음을 고려하기에 충분해야합니다. 이벤트에 반응하는 Redux 스토어가 있다는 사실은 선택적 구현 세부 사항입니다.
Sebastien Lorber

3
나는 누군가 자신의 도서관을 판매하기 위해 실제 질문에서 산만하기 때문에이 답변을 좋아하지 않습니다. 이 답변은 두 라이브러리의 비교를 제공하지만 질문의 의도는 아닙니다. 실제 질문은 미들웨어의 사용 여부를 묻는 것입니다.
Abhinav Manchanda

31

짧은 대답 : 나에게 비동기 문제에 완전히 합리적인 접근법처럼 보입니다. 몇 가지 경고가 있습니다.

우리가 방금 시작한 새 프로젝트를 수행 할 때 비슷한 생각을했습니다. 나는 React 컴포넌트 트리의 장에서 벗어나는 방식으로 저장소를 업데이트하고 컴포넌트를 다시 렌더링하는 바닐라 Redux의 우아한 시스템을 좋아했습니다. dispatch비동기 처리를위한 우아한 메커니즘에 연결하는 것이 이상해 보였습니다 .

나는 프로젝트에서 제외시킨 라이브러리에있는 것과 비슷한 접근법을 사용했으며, 우리는 react-redux-controller라고했습니다 .

나는 두 가지 이유로 위의 정확한 접근 방식을 사용하지 않았습니다.

  1. 당신이 작성한 방식대로, 파견 기능은 상점에 액세스 할 수 없습니다. 디스패치 기능에 필요한 모든 정보를 UI 구성 요소에 전달하면 문제를 해결할 수 있습니다. 그러나 이것이 UI 구성 요소를 디스패치 논리에 불필요하게 결합한다고 주장합니다. 더 문제 적으로, 디스패치 기능이 비동기 연속으로 업데이트 된 상태에 액세스 할 수있는 확실한 방법은 없습니다.
  2. 디스패치 함수는 dispatch어휘 범위를 통해 자체에 액세스 할 수 있습니다. 이것은 그 connect문장이 손 에 닿으면 리팩토링의 옵션을 제한합니다 update. 따라서 이러한 디스패처 기능을 별도의 모듈로 분리 할 수 ​​있도록하는 시스템이 필요합니다.

함께 모아서 dispatch이벤트 매개 변수와 함께 디스패치 기능에 상점을 주입하고 저장 하도록 일부 시스템을 준비 해야합니다. 이 의존성 주입에 대한 세 가지 합리적인 접근법을 알고 있습니다.

  • redux-thunk 는 기능을 수행하여 썽크에 전달하여 (정확히 벙크가 아닌 돔 정의로)이 기능을 수행합니다. 다른 dispatch미들웨어 접근 방식으로 작업하지는 않았지만 기본적으로 동일하다고 가정합니다.
  • react-redux-controller는 코 루틴으로이를 수행합니다. 또한 보너스로 "선택자"에 액세스 할 수도 있습니다. "선택기"는 connect정규화 된 원시 상점과 직접 작업하지 않고 첫 번째 인수로 전달한 기능 입니다.
  • 또한 this다양한 가능한 메커니즘을 통해 컨텍스트 에 삽입하여 객체 지향 방식으로 수행 할 수 있습니다 .

최신 정보

이 수수께끼의 일부는 react-redux 의 제한 사항입니다 . connect상태 스냅 샷 을 가져 오지만 전달하지 않는 첫 번째 인수 입니다. 두 번째 인수는 전달되지만 상태는 아닙니다. 연속 / 콜백시 업데이트 된 상태를 볼 수 있으므로 현재 상태를 닫는 썽크를 인수로받지 않습니다.


22

Abramov의 목표는 모든 사람이 이상적으로 가장 복잡한 곳에 복잡한 (및 비동기 호출) 캡슐화를하는 것 입니다.

표준 Redux 데이터 흐름에서 가장 좋은 곳은 어디입니까? 어때요?

  • 감속기 ? 절대 안돼. 부작용이없는 순수한 기능이어야합니다. 상점 업데이트는 심각하고 복잡한 업무입니다. 오염시키지 마십시오.
  • 벙어리보기 구성 요소? 분명히 아닙니다. 프레젠테이션과 사용자 상호 작용에 대한 한 가지 우려가 있으며 가능한 한 단순해야합니다.
  • 컨테이너 구성 요소? 가능하지만 차선책입니다. 컨테이너는 뷰 관련 복잡성을 캡슐화하고 상점과 상호 작용하는 장소라는 점에서 의미가 있습니다.
    • 컨테이너는 벙어리 구성 요소보다 복잡해야하지만 뷰와 상태 / 저장소간에 바인딩을 제공하는 것은 여전히 ​​단일 책임입니다. 비동기 로직은 그것과는 완전히 별개입니다.
    • 컨테이너에 배치하면 단일보기 / 경로를 위해 비동기 논리를 단일 컨텍스트로 잠글 수 있습니다. 나쁜 생각. 이상적으로는 모두 재사용 가능하고 완전히 분리되어 있습니다.
  • S 오메 다른 서비스 모듈은? 나쁜 생각 : 유지 관리 / 테스트 가능성 악몽 인 상점에 대한 액세스 권한을 주입해야합니다. Redux를 사용하고 제공된 API / 모델 만 사용하여 상점에 액세스하는 것이 좋습니다.
  • 이를 해석하는 조치 및 미들웨어? 왜 안돼?! 우선, 우리가 남긴 유일한 주요 옵션입니다. :-)보다 논리적으로, 액션 시스템은 어느 곳에서나 사용할 수있는 분리 된 실행 로직입니다. 상점에 액세스 할 수 있으며 추가 조치를 전달할 수 있습니다. 애플리케이션 주위의 제어 및 데이터 흐름을 구성하는 것은 단일 책임이며, 대부분의 비동기는 그에 맞습니다.
    • 액션 크리에이터는 어떻습니까? 액션 자체와 미들웨어 대신 비동기식으로 작업하는 것이 어떻습니까?
      • 가장 중요한 것은 제작자가 미들웨어처럼 상점에 액세스 할 수 없다는 것입니다. 즉, 새로운 우발적 인 행동을 파견 할 수 없으며 상점에서 읽을 수 없어 비동기를 구성 할 수 있습니다.
      • 따라서 복잡성이 필요한 장소에 복잡성을 유지하고 다른 모든 것을 단순하게 유지하십시오. 그러면 제작자는 테스트하기 쉬운 단순하고 비교적 순수한 기능 일 수 있습니다.

컨테이너 구성 요소 -왜 안됩니까? React에서 컴포넌트가 수행하는 역할로 인해 컨테이너는 서비스 클래스 역할을 할 수 있으며 이미 DI (props)를 통해 상점을 가져옵니다. 컨테이너에 배치하면 단일보기 / 경로를 위해 비동기 논리를 단일 컨텍스트로 잠 그게됩니다. 컴포넌트는 여러 인스턴스를 가질 수 있습니다. 렌더링 소품과 같이 프리젠 테이션에서 분리 할 수 ​​있습니다. 나는 그 대답이 요점을 입증하는 짧은 예에서 훨씬 더 도움이 될 수 있다고 생각합니다.
Estus Flask

나는이 대답을 좋아한다!
Mauricio Avendaño

13

처음에 묻는 질문에 대답하려면 :

컨테이너 구성 요소가 비동기 API를 호출 한 다음 작업을 전달할 수없는 이유는 무엇입니까?

이러한 문서는 Redux plus React가 아니라 Redux 용입니다. React 컴포넌트에 연결된 Redux 스토어는 사용자가하는 말을 정확하게 수행 할 수 있지만 미들웨어가없는 Plain Jane Redux 스토어는 dispatch일반 객체 를 제외하고 인수를 허용하지 않습니다 .

미들웨어가 없으면 여전히 할 수 있습니다.

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

하지만 그것은 비동기 래핑 된 유사한 사건 의 주위에 돌아 오는 것이 아니라 처리 돌아 오는이. 따라서 미들웨어는에 직접 전달할 수있는 항목을 수정하여 비동기 성을 허용합니다 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비동기 인지 알 수 없습니다 .

약속 , 관찰 가능 , sagas 또는 미친 사용자 정의매우 선언적인 액션 제작자 를 지원하려는 경우 Redux는 전달하는 내용 dispatch(즉, 액션 제작자에서 반환 하는 것)을 변경하여 수행 할 수 있습니다 . React 컴포넌트 (또는 connect호출)를 사용하지 않아도됩니다.


조치 완료시 또 다른 이벤트를 전달하는 것이 좋습니다. 작업 완료 후 alert ()을 표시해야 할 때 작동하지 않습니다. React 구성 요소 내부의 약속은 작동합니다. 나는 현재 Promises 접근법을 권장합니다.
catamphetamine

8

확인, 미들웨어가 어떻게 작동하는지 먼저 살펴 보도록하겠습니다. 질문에 대한 답입니다. 이것은 Redux 의 pplyMiddleWare 함수 소스 코드입니다 .

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);
}
  • 각 미들웨어에는 dispatchgetState 이름이 지정된 인수로 및 기능이 제공됩니다.

좋아,이게 어떻게 Redux 에 가장 많이 사용되는 미들웨어 중 하나 인 Redux-thunk 가 자체적으로 소개하는 방식입니다.

Redux Thunk 미들웨어를 사용하면 조치 대신 함수를 리턴하는 조치 작성자를 작성할 수 있습니다. 썽 크는 조치의 디스패치를 ​​지연 시키거나 특정 조건이 충족되는 경우에만 디스패치하는 데 사용될 수 있습니다. 내부 함수는 store 메소드 dispatch 및 getState를 매개 변수로받습니다.

보시다시피, 액션 대신 함수를 반환합니다. 함수이므로 언제든지 호출하고 기다릴 수 있습니다.

도대체 뭐야? 그것이 Wikipedia에 소개 된 방법입니다.

컴퓨터 프로그래밍에서 썽 크는 추가 계산을 다른 서브 루틴에 주입하는 데 사용되는 서브 루틴입니다. 썽 크는 주로 필요할 때까지 계산을 지연 시키거나 다른 서브 루틴의 시작 또는 끝에 연산을 삽입하는 데 사용됩니다. 이들은 컴파일러 코드 생성 및 모듈 식 프로그래밍을위한 다양한 다른 응용 프로그램을 가지고 있습니다.

이 용어는 "생각하는"의 파생적 파생어에서 유래했습니다.

썽 크는 식을 래핑하여 평가를 지연시키는 함수입니다.

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

따라서 개념이 얼마나 쉬운 지, 비동기 작업을 관리하는 데 어떻게 도움이되는지 확인하십시오 ...

그것은 당신이 그것없이 살 수있는 일이지만, 프로그래밍에는 항상 더 좋고 깔끔하며 적절한 방법이 있습니다.

미들웨어 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입니다. 우리는 백엔드를 호출하여 프론트 엔드에서 사용할 수있는 데이터를 제공해야합니다. 이것이가는 길이라면, 뭔가 잘못
되었거나

안녕하세요, API 호출에 try 및 catch 블록을 사용하십시오. API가 응답을 제공하면 Reducer 조치 유형을 호출하십시오.
SM Chinna

1
@zameb 당신이 옳을 수도 있지만, 불만은 Redux 자체와 복잡성을 줄이기 위해 노력하는 동안 들려 온 것입니다.
jorisw

1

동기 조치 작성자가 있으며 비동기 조치 작성자가 있습니다.

동기 액션 생성기는 호출 할 때 해당 객체에 연결된 모든 관련 데이터와 함께 감속기가 처리 할 준비가 된 Action 객체를 즉시 반환합니다.

비동기 액션 제작자는 액션을 디스패치하기 전에 약간의 시간이 소요됩니다.

정의에 따라 네트워크 요청을하는 액션 생성자가 있으면 항상 비동기 액션 생성자로 자격이 부여됩니다.

Redux 응용 프로그램 내부에 비동기 작업 생성자를 사용하려면 해당 비동기 작업 생성자를 처리 할 수있는 미들웨어라는 것을 설치해야합니다.

비동기 작업에 사용자 지정 미들웨어를 사용한다는 오류 메시지에서이를 확인할 수 있습니다.

그렇다면 미들웨어 란 무엇이며 왜 Redux의 비동기 흐름에 필요합니까?

redux-thunk와 같은 redux 미들웨어와 관련하여 미들웨어는 비동기 작업 작성기를 처리하는 데 도움이되므로 Redux는 즉시 처리 할 수 ​​없습니다.

Redux주기에 미들웨어가 통합 된 상태에서, 우리는 여전히 액션 생성자를 호출하고 있습니다. 이는 액션이 ​​전달 될 것입니다. 그러나 액션을 디스패치 할 때 모든 리듀서로 직접 전송하는 대신 액션을 디스패치 할 때, 애플리케이션 내부의 모든 다른 미들웨어를 통해 조치가 전송된다고 말하십시오.

단일 Redux 앱 내에서 원하는만큼 미들웨어를 가질 수 있습니다. 대부분의 경우, 우리가 작업하는 프로젝트에서 Redux 스토어에 하나 또는 두 개의 미들웨어가 연결됩니다.

미들웨어는 우리가 파견 할 때마다 호출되는 일반 JavaScript 함수입니다. 이 기능 내에서 미들웨어는 액션이 ​​리듀서로 전달되는 것을 막을 수 있습니다. 예를 들어 콘솔이 로그하는 미들웨어를 생성 할 수있는 방식으로 액션을 수정하거나 액션과 혼동 할 수 있습니다. 보는 즐거움을 위해 파견하는 모든 행동.

프로젝트에 종속성으로 설치할 수있는 수많은 오픈 소스 미들웨어가 있습니다.

오픈 소스 미들웨어를 사용하거나 종속성으로 설치하는 것에 만 국한되지 않습니다. 사용자 정의 미들웨어를 작성하여 Redux 상점 내부에서 사용할 수 있습니다.

미들웨어의 가장 보편적 인 용도 중 하나 (및 답변을 얻는 것)는 비동기 액션 제작자를 다루는 것입니다. 아마도 가장 인기있는 미들웨어는 리덕 스턱이고 비동기 액션 제작자를 다루는 데 도움이됩니다.

비동기 액션 제작자를 다루는 데 도움이되는 다른 많은 유형의 미들웨어가 있습니다.


1

질문에 대답하려면 :

컨테이너 구성 요소가 비동기 API를 호출 한 다음 작업을 전달할 수없는 이유는 무엇입니까?

적어도 두 가지 이유로 말할 것입니다.

첫 번째 이유는 그것의 일이 아니다, 문제의 분리입니다 action creator를 호출 api및 데이터 다시 얻을, 당신은 당신의 두 인수를 전달해야 할 필요 action creator functionaction typepayload.

두 번째 이유는 redux store필수 조치 유형 및 선택 사항 인 일반 오브젝트를 대기 중이기 때문 입니다 payload(그러나 여기서 페이로드도 전달해야 함).

액션 크리에이터는 아래와 같은 일반 객체 여야합니다.

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

그리고하는 일 Redux-Thunk midlewaredispache당신의 결과 api call적절한에 action.


0

엔터프라이즈 프로젝트에서 작업 할 때 간단한 비동기식 흐름에서는 사용할 수없는 (saga)와 같은 미들웨어에서 사용할 수있는 많은 요구 사항이 있습니다.

  • 병렬로 요청 실행
  • 기다릴 필요없이 미래의 행동
  • 비 차단 통화 레이스 효과, 예 픽업 우선
  • 프로세스 시작을위한 응답 작업 순서 지정 (첫 번째 호출에서 첫 번째)
  • 식자
  • 작업 취소 작업을 동적으로 분기합니다.
  • redux 미들웨어 외부에서 Saga를 실행하는 동시성을 지원하십시오.
  • 채널 사용

목록은 오랫동안 saga 문서 의 고급 섹션을 검토합니다.

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