Dlaczego potrzebujemy oprogramowania pośredniego dla przepływu asynchronicznego w Redux?


686

Według dokumentów: „Bez oprogramowania pośredniego sklep Redux obsługuje tylko synchroniczny przepływ danych” . Nie rozumiem, dlaczego tak jest. Dlaczego składnik kontenera nie może wywołać interfejsu API asynchronicznego, a następnie dispatchakcji?

Na przykład wyobraź sobie prosty interfejs użytkownika: pole i przycisk. Gdy użytkownik naciska przycisk, pole zostaje zapełnione danymi ze zdalnego serwera.

Pole i przycisk

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

Po wyrenderowaniu eksportowanego komponentu mogę kliknąć przycisk, a dane wejściowe zostaną poprawnie zaktualizowane.

Zanotuj updatefunkcję w connectpołączeniu. Wysyła akcję informującą aplikację o aktualizacji, a następnie wykonuje połączenie asynchroniczne. Po zakończeniu połączenia podana wartość jest wysyłana jako ładunek innego działania.

Co jest złego w tym podejściu? Dlaczego miałbym chcieć używać Redux Thunk lub Redux Promise, jak sugeruje dokumentacja?

EDYCJA: Przeszukałem repozytorium Redux w poszukiwaniu wskazówek i odkryłem, że Action Creators w przeszłości wymagały czystych funkcji. Na przykład użytkownik próbuje lepiej wyjaśnić przepływ danych asynchronicznych:

Sam twórca akcji jest nadal czystą funkcją, ale zwięzła funkcja, którą zwraca, nie musi być i może wykonywać nasze wywołania asynchroniczne

Twórcy akcji nie są już zobowiązani do zachowania czystości. Tak więc w przeszłości zdecydowanie wymagane było oprogramowanie pośrednie typu thunk / obiec, ale wydaje się, że tak już nie jest?


53
Twórcy akcji nigdy nie musieli być czystymi funkcjami. To był błąd w dokumentacji, a nie decyzja, która się zmieniła.
Dan Abramov

1
@DanAbramov dla testowalności może być jednak dobrą praktyką. Saga Redux pozwala na to: stackoverflow.com/a/34623840/82609
Sebastien Lorber

Odpowiedzi:


701

Co jest złego w tym podejściu? Dlaczego miałbym chcieć używać Redux Thunk lub Redux Promise, jak sugeruje dokumentacja?

W tym podejściu nie ma nic złego. Jest to po prostu niewygodne w dużej aplikacji, ponieważ będziesz mieć różne komponenty wykonujące te same akcje, możesz chcieć ogłosić niektóre akcje lub utrzymać stan lokalny, taki jak automatyczne zwiększanie identyfikatorów blisko twórców akcji itp. Więc jest to po prostu łatwiejsze od punkt widzenia utrzymania, aby wyodrębnić twórców akcji do oddzielnych funkcji.

Możesz przeczytać moją odpowiedź na „Jak wysłać akcję Redux z limitem czasu”, aby uzyskać bardziej szczegółowy przewodnik.

Oprogramowanie pośrednie, takie jak Redux Thunk lub Redux Promise, po prostu daje „cukier składniowy” do wysyłania thunks lub obietnic, ale nie trzeba go używać.

Więc bez pośredniego oprogramowania może wyglądać Twój twórca akcji

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

Ale dzięki Thunk Middleware możesz to napisać w następujący sposób:

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

Więc nie ma ogromnej różnicy. Jedną z rzeczy, które lubię w tym drugim podejściu, jest to, że komponent nie dba o to, by twórca akcji był asynchroniczny. Po prostu wywołuje dispatchnormalnie, można go również użyć mapDispatchToPropsdo powiązania takiego twórcy akcji krótką składnią itp. Składniki nie wiedzą, jak są implementowane twórcy akcji, i można przełączać się między różnymi podejściami asynchronicznymi (Redux Thunk, Redux Promise, Redux Saga ) bez zmiany komponentów. Z drugiej strony, przy pierwszym, jawnym podejściu, komponenty wiedzą dokładnie, że określone wywołanie jest asynchroniczne i musi dispatchzostać przekazane przez pewną konwencję (na przykład jako parametr synchronizacji).

Zastanów się także, jak ten kod się zmieni. Powiedzmy, że chcemy mieć drugą funkcję ładowania danych i połączyć je w jednym kreatorze akcji.

Przy pierwszym podejściu musimy pamiętać o tym, jakiego rodzaju twórcę akcji nazywamy:

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

Dzięki Redux Thunk twórcy akcji mogą dispatchwynikać z działania innych twórców akcji, a nawet nie myśleć, czy są oni synchroniczni czy asynchroniczni:

// 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!
}

Dzięki takiemu podejściu, jeśli później chcesz, aby twórcy akcji sprawdzili aktualny stan Redux, możesz po prostu użyć drugiego getStateargumentu przekazanego thunksom bez modyfikacji kodu wywołującego:

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

Jeśli chcesz go zmienić, aby był synchroniczny, możesz to zrobić również bez zmiany kodu wywoławczego:

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

Zaletą korzystania z oprogramowania pośredniego, takiego jak Redux Thunk lub Redux Promise, jest to, że komponenty nie są świadome, w jaki sposób implementowani są twórcy akcji i czy dbają o stan Redux, czy są synchroniczne czy asynchroniczne oraz czy wywołują innych twórców akcji . Minusem jest trochę pośredniość, ale uważamy, że warto w prawdziwych zastosowaniach.

Wreszcie, Redux Thunk i przyjaciele to tylko jedno z możliwych podejść do asynchronicznych żądań w aplikacjach Redux. Innym interesującym podejściem jest Saga Redux, która pozwala definiować długo działające demony („sagi”), które podejmują działania w miarę ich pojawiania się oraz przekształcają lub wykonują żądania przed wydaniem działań. To przenosi logikę z twórców akcji do sag. Możesz to sprawdzić, a później wybrać to, co najbardziej Ci odpowiada.

Przeszukałem repozytorium Redux w poszukiwaniu wskazówek i odkryłem, że w przeszłości twórcy akcji musieli pełnić wyłącznie funkcje.

To jest niepoprawne. Dokumenty to powiedziały, ale dokumenty były błędne.
Twórcy akcji nigdy nie musieli być czystymi funkcjami.
Naprawiliśmy dokumenty, aby to odzwierciedlić.


57
Być może w skrócie można powiedzieć, że Dan pomyślał: oprogramowanie pośrednie to scentralizowane podejście, w ten sposób pozwala uprościć i uogólnić komponenty oraz kontrolować przepływ danych w jednym miejscu. Jeśli utrzymasz dużą aplikację, spodoba ci się =)
Sergey Lapin

3
@asdfasdfads Nie rozumiem, dlaczego to nie działa. To działałoby dokładnie tak samo; umieścić alertpo dispatch()akcji.
Dan Abramov

9
Przedostatni wiersz w pierwszym przykładzie kodu: loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch. Dlaczego muszę przekazać przesyłkę? Jeśli zgodnie z konwencją istnieje tylko jeden globalny sklep, dlaczego po prostu nie odwołuję się do tego bezpośrednio i robię to store.dispatchza każdym razem , gdy muszę, np. Wejść loadData?
Søren Debois

10
@ SørenDebois Jeśli Twoja aplikacja jest po stronie klienta, tylko to by działało. Jeśli jest renderowany na serwerze, będziesz chciał mieć inną storeinstancję dla każdego żądania, abyś nie mógł go wcześniej zdefiniować.
Dan Abramov,

3
Chcę tylko zaznaczyć, że ta odpowiedź ma 139 linii, czyli 9,92 razy więcej niż kod źródłowy redux-thunk, który składa się z 14 linii: github.com/gaearon/redux-thunk/blob/master/src/index.js
Guy

447

Ty nie.

Ale ... powinieneś użyć redux-saga :)

Odpowiedź Dana Abramova jest słuszna, redux-thunkale powiem trochę więcej o sadze redux, która jest dość podobna, ale mocniejsza.

Tryb rozkazujący a deklaratywny

  • DOM : jQuery jest konieczny / React jest deklaratywny
  • Monady : IO jest konieczne / Free jest deklaratywne
  • Efekty dodatkowe : redux-thunkjest konieczny / redux-sagadeklaratywny

Kiedy masz w ręku kutasa, jak monada IO lub obietnica, nie możesz łatwo wiedzieć, co zrobi po wykonaniu. Jedynym sposobem przetestowania thunk jest wykonanie go i wyśmiewanie się z dyspozytora (lub całego świata zewnętrznego, jeśli wchodzi on w interakcje z większą ilością rzeczy ...).

Jeśli korzystasz z prób, nie wykonujesz programowania funkcjonalnego.

Widziane przez pryzmat efektów ubocznych, kpiny są flagą, że kod jest nieczysty, aw oczach funkcjonalnego programisty dowód, że coś jest nie tak. Zamiast pobierać bibliotekę, która pomoże nam sprawdzić, czy góra lodowa jest nienaruszona, powinniśmy żeglować wokół niej. Ostry facet TDD / Java zapytał mnie kiedyś, jak robisz drwiny w Clojure. Odpowiedź brzmi: zwykle nie. Zwykle postrzegamy to jako znak, że musimy zmienić kod naszego kodu.

Źródło

Sagi (po ich zaimplementowaniu redux-saga) są deklaratywne i podobnie jak komponenty Free monad lub React, są znacznie łatwiejsze do przetestowania bez żadnego próbnego.

Zobacz także ten artykuł :

we współczesnym FP nie powinniśmy pisać programów - powinniśmy pisać opisy programów, które możemy następnie introspekcjonować, przekształcać i interpretować do woli.

(W rzeczywistości saga Redux jest jak hybryda: przepływ jest konieczny, ale efekty są deklaratywne)

Zamieszanie: akcje / zdarzenia / polecenia ...

W świecie frontendu istnieje wiele nieporozumień co do tego, w jaki sposób niektóre koncepcje backendu, takie jak CQRS / EventSourcing i Flux / Redux, mogą być powiązane, głównie dlatego, że w Flux używamy terminu „akcja”, który może czasami reprezentować zarówno imperatywny kod ( LOAD_USER), jak i zdarzenia ( USER_LOADED). Uważam, że podobnie jak pozyskiwanie wydarzeń, powinieneś wysyłać tylko zdarzenia.

Wykorzystywanie sag w praktyce

Wyobraź sobie aplikację z linkiem do profilu użytkownika. Idiomatycznym sposobem radzenia sobie z tym z każdym oprogramowaniem pośrednim byłoby:

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

Ta saga przekłada się na:

za każdym razem, gdy użytkownik kliknie nazwę użytkownika, pobierz profil użytkownika, a następnie wywołaj zdarzenie z załadowanym profilem.

Jak widać, istnieją pewne zalety redux-saga.

Użycie takeLatestzezwoleń na wyrażenie, że jesteś zainteresowany jedynie uzyskaniem danych o ostatniej klikniętej nazwie użytkownika (radzisz sobie z problemami dotyczącymi współbieżności w przypadku, gdy użytkownik kliknie bardzo szybko wiele nazw użytkowników). Tego rodzaju rzeczy są trudne do zrobienia. Możesz użyć, takeEveryjeśli nie chcesz tego zachowania.

Dbasz o czystość twórców akcji. Pamiętaj, że nadal warto zachować ActionCreators (w sagach puti komponentach dispatch), ponieważ może to pomóc w dodaniu sprawdzania poprawności akcji (asercje / przepływ / maszynopis) w przyszłości.

Twój kod staje się znacznie bardziej testowalny, ponieważ efekty są deklaratywne

Już nie potrzebujesz, aby wywoływać połączenia podobne do RPC actions.loadUser(). Twój interfejs użytkownika musi tylko wysłać to, co się stało. Odpalamy tylko zdarzenia (zawsze w czasie przeszłym!), A nie akcje. Oznacza to, że możesz tworzyć oddzielone „kaczki” lub ograniczone konteksty i że saga może działać jako punkt sprzężenia między tymi modułowymi elementami.

Oznacza to, że Twoje widoki są łatwiejsze do zarządzania, ponieważ nie muszą już zawierać tej warstwy tłumaczenia między tym, co się wydarzyło, a tym, co powinno się stać efektem

Na przykład wyobraź sobie nieskończony widok przewijania. CONTAINER_SCROLLEDmoże prowadzić do NEXT_PAGE_LOADED, ale czy tak naprawdę to na przewijanym kontenerze spoczywa decyzja, czy należy załadować inną stronę? Następnie musi zdawać sobie sprawę z bardziej skomplikowanych rzeczy, takich jak to, czy ostatnia strona została pomyślnie załadowana, czy jest już strona, która próbuje się załadować, czy też nie ma już więcej elementów do załadowania? Nie sądzę: dla maksymalnej możliwości wielokrotnego użytku przewijany pojemnik powinien po prostu opisać, że został przewinięty. Ładowanie strony jest „efektem biznesowym” tego zwoju

Niektórzy mogą argumentować, że generatory mogą z natury ukrywać stan poza magazynem redux za pomocą zmiennych lokalnych, ale jeśli zaczniesz aranżować skomplikowane rzeczy wewnątrz thunks poprzez uruchomienie timerów itp., I tak będziesz miał ten sam problem. I istnieje selectefekt, który pozwala teraz uzyskać pewien stan ze sklepu Redux.

Sagi można podróżować w czasie, a także umożliwia złożone rejestrowanie przepływu i narzędzia deweloperskie, nad którymi obecnie pracujemy. Oto kilka prostych zapisów przepływu asynchronicznego, które zostały już zaimplementowane:

rejestrowanie przepływu saga

Oddzielenie

Sagi nie tylko zastępują redux thunks. Pochodzą z backendu / systemów rozproszonych / pozyskiwania zdarzeń.

Bardzo powszechne jest błędne przekonanie, że sagi są tutaj po to, aby zastąpić twoje redukcje lepszymi testami. W rzeczywistości jest to tylko szczegół implementacji redux-sagi. Użycie efektów deklaratywnych jest lepsze niż testowanie, ale wzorzec sagi można zaimplementować na podstawie kodu rozkazującego lub deklaratywnego.

Po pierwsze, saga to oprogramowanie, które pozwala na koordynację długo trwających transakcji (ostateczna spójność) i transakcji w różnych kontekstach (żargon projektowy oparty na domenie).

Aby uprościć to dla frontendowego świata, wyobraź sobie, że jest widget1 i widget2. Kliknięcie przycisku na widżecie1 powinno mieć wpływ na widżet2. Zamiast łączyć dwa widżety razem (tj. Widget1 wywołuje akcję skierowaną na widget2), widget1 wywołuje tylko kliknięcie jego przycisku. Następnie saga nasłuchuje kliknięcia tego przycisku, a następnie aktualizuje widget2, usuwając nowe zdarzenie, o którym widget2 jest świadomy.

Dodaje to poziomu pośrednictwa, który nie jest konieczny w przypadku prostych aplikacji, ale ułatwia skalowanie złożonych aplikacji. Teraz możesz publikować widget1 i widget2 w różnych repozytoriach npm, aby nigdy nie musieli się o sobie znać bez konieczności udostępniania im globalnego rejestru działań. Dwa widżety są teraz ograniczonymi kontekstami, które mogą żyć osobno. Nie muszą się nawzajem zachowywać spójności i mogą być ponownie wykorzystywane w innych aplikacjach. Saga jest punktem połączenia między dwoma widżetami, które koordynują je w znaczący sposób dla Twojej firmy.

Kilka ciekawych artykułów na temat struktury aplikacji Redux, na których możesz używać sagi Redux z powodów oddzielania:

Konkretny przypadek użycia: system powiadomień

Chcę, aby moje komponenty mogły wyświetlać powiadomienia w aplikacji. Ale nie chcę, aby moje komponenty były silnie sprzężone z systemem powiadomień, który ma własne reguły biznesowe (maks. 3 powiadomienia wyświetlane jednocześnie, kolejkowanie powiadomień, czas wyświetlania 4 sekundy itp.).

Nie chcę, aby moje komponenty JSX decydowały, kiedy powiadomienie zostanie wyświetlone / ukryte. Po prostu daję mu możliwość zażądania powiadomienia i pozostawienie skomplikowanych zasad w sadze. Tego rodzaju rzeczy są dość trudne do zrealizowania za pomocą gromad lub obietnic.

powiadomienia

Opisałem tutaj, jak można to zrobić za pomocą sagi

Dlaczego nazywa się Saga?

Termin saga pochodzi ze świata zaplecza. Początkowo wprowadziłem do tego terminu Yassine (autor redagi-sagi) w długiej dyskusji .

Początkowo termin ten został wprowadzony w formie papierowej , wzorzec sagi miał być używany do obsługi ostatecznej spójności w transakcjach rozproszonych, ale jego użycie zostało rozszerzone na szerszą definicję przez programistów backendu, tak że teraz obejmuje również „menedżera procesów” wzorzec (w jakiś sposób oryginalny wzorzec sagi jest wyspecjalizowaną formą menedżera procesów).

Dzisiaj termin „saga” jest mylący, ponieważ może opisać 2 różne rzeczy. Używany w sadze redux nie opisuje sposobu obsługi transakcji rozproszonych, ale raczej sposób koordynacji działań w Twojej aplikacji. redux-sagamożna by również nazwać redux-process-manager.

Zobacz też:

Alternatywy

Jeśli nie podoba ci się pomysł użycia generatorów, ale interesuje Cię wzorzec sagi i jego właściwości oddzielające, możesz również osiągnąć to samo z obserwowalnym redux, który używa nazwy epicdo opisania dokładnie tego samego wzorca, ale z RxJS. Jeśli znasz już Rx, poczujesz się jak w domu.

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

Niektóre przydatne sagi redux

2017 r

  • Nie nadużywaj sagi Redux tylko ze względu na jej użycie. Tylko sprawdzalne wywołania API nie są tego warte.
  • Nie usuwaj grudek z twojego projektu w najprostszych przypadkach.
  • Nie wahaj się wysyłać thunksów, yield put(someActionThunk)jeśli ma to sens.

Jeśli boisz się używać sagi Redux (lub obserwowalnej w Redux), ale potrzebujesz jedynie wzoru oddzielenia, sprawdź redux-dispatch-subscribe : pozwala na słuchanie wysyłek i wyzwalanie nowych wysyłek w słuchaczu.

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

64
Z każdą kolejną wizytą jest coraz lepiej. Zastanów się, czy nie zamienić go w post na blogu :).
RainerAtSpirit

4
Dziękuję za dobry napis. Jednak nie zgadzam się co do niektórych aspektów. Jak konieczne jest LOAD_USER? Dla mnie jest to nie tylko deklaratywne - daje również świetny czytelny kod. Jak np. „Po naciśnięciu tego przycisku chcę ADD_ITEM”. Mogę spojrzeć na kod i dokładnie zrozumieć, co się dzieje. Gdyby zamiast tego nazwano coś tak, jak efekt „BUTTON_CLICK”, musiałbym to sprawdzić.
ściągnij

4
Niezła odpowiedź. Istnieje teraz inna alternatywa: github.com/blesh/redux-observable
swennemen

4
@swelet przepraszam za spóźnione odpowiedzi. Kiedy wysyłasz ADD_ITEM, jest to konieczne, ponieważ wysyłasz akcję, która ma na celu wywarcie wpływu na Twój sklep: oczekujesz, że akcja coś zrobi. Będąc deklaratywnym, zastosuj filozofię pozyskiwania zdarzeń: nie wywołujesz działań w celu wywołania zmian w swoich aplikacjach, ale wysyłasz zdarzenia z przeszłości, aby opisać, co się stało z twoją aplikacją. Wysłanie zdarzenia powinno wystarczyć, aby uznać, że stan aplikacji zmienił się. Fakt, że istnieje sklep Redux, który reaguje na zdarzenie, jest opcjonalnym szczegółem implementacji
Sebastien Lorber

3
Nie podoba mi się ta odpowiedź, ponieważ odwraca ona uwagę od rzeczywistego pytania, aby sprzedać komuś własną bibliotekę. Ta odpowiedź zapewnia porównanie dwóch bibliotek, co nie było celem pytania. Rzeczywiste pytanie dotyczy tego, czy w ogóle korzystać z oprogramowania pośredniego, co tłumaczy przyjęta odpowiedź.
Abhinav Manchanda

31

Krótka odpowiedź : wydaje mi się całkowicie rozsądnym podejściem do problemu asynchronicznego. Z kilkoma zastrzeżeniami.

Miałem bardzo podobny tok myślenia podczas pracy nad nowym projektem, który właśnie rozpoczęliśmy w mojej pracy. Byłem wielkim fanem eleganckiego systemu waniliowego Redux do aktualizowania sklepu i ponownego wysyłania komponentów w sposób, który pozostaje poza wnętrzem drzewa komponentów React. Wydawało mi się dziwne podłączenie się do tego eleganckiego dispatchmechanizmu do obsługi asynchronii.

Skończyłem z bardzo podobnym podejściem do tego, co masz w bibliotece, którą uwzględniłem w naszym projekcie, który nazwaliśmy reakcją-kontrolerem-reduksem .

Skończyło się na tym, że nie podążyłem dokładnie takim podejściem z kilku powodów:

  1. Sposób, w jaki został napisany, te funkcje wysyłające nie mają dostępu do sklepu. Można to nieco obejść, przekazując komponentom interfejsu użytkownika wszystkie informacje potrzebne funkcji wysyłającej. Twierdzę jednak, że niepotrzebnie łączy te elementy interfejsu użytkownika z logiką wysyłającą. I bardziej problematycznie, nie ma oczywistego sposobu, aby funkcja dyspozytorska miała dostęp do zaktualizowanego stanu w kontynuacjach asynchronicznych.
  2. Funkcje wysyłające mają dostęp do dispatchsiebie poprzez zakres leksykalny. Ogranicza to opcje refaktoryzacji, gdy connectinstrukcja wymknie się spod kontroli - i wygląda to dość niewygodnie z tą jedną updatemetodą. Potrzebujesz więc systemu umożliwiającego komponowanie funkcji dyspozytora, jeśli podzielisz je na osobne moduły.

Podsumowując, musisz skonfigurować jakiś system, aby zezwolić dispatchi sklep zostanie wprowadzony do twoich funkcji wysyłkowych, wraz z parametrami zdarzenia. Znam trzy rozsądne podejścia do tego zastrzyku zależności:

  • redux-thunk robi to w funkcjonalny sposób, przekazując je do twoich thunków (czyniąc je niezupełnie thunkami, według definicji kopuły). Nie pracowałem z innymi dispatchmetodami oprogramowania pośredniego, ale zakładam, że są one w zasadzie takie same.
  • Reakcja-kontroler-redux robi to z korupcją. Jako bonus daje ci również dostęp do „selektorów”, które są funkcjami, które mogłeś przekazać jako pierwszy argument connect, zamiast konieczności bezpośredniej pracy z surowym, znormalizowanym sklepem.
  • Można to również zrobić w sposób zorientowany obiektowo, wstrzykując je do thiskontekstu za pomocą różnych możliwych mechanizmów.

Aktualizacja

Przyszło mi do głowy, że część tej zagadki jest ograniczeniem ponownego reagowania . Pierwszy argument, który connectpobiera migawkę stanu, ale nie wysyła. Drugi argument zostanie wysłany, ale nie stan. Żaden argument nie dostaje thunk, który zamyka się nad bieżącym stanem, za możliwość zobaczenia zaktualizowanego stanu w czasie kontynuacji / oddzwaniania.


22

Celem Abramova - i idealnie dla wszystkich - jest po prostu hermetyzacja złożoności (i asynchronicznych połączeń) w miejscu, w którym jest to najbardziej odpowiednie .

Gdzie najlepiej to zrobić w standardowym przepływie danych Redux? Co powiesz na:

  • Reduktory ? Nie ma mowy. Powinny być czystymi funkcjami bez skutków ubocznych. Aktualizacja sklepu to poważna, skomplikowana sprawa. Nie zanieczyszczaj tego.
  • Głupi widok komponentów? Zdecydowanie nie. Mają jeden problem: prezentację i interakcję z użytkownikiem i powinny być tak proste, jak to możliwe.
  • Komponenty do kontenerów? Możliwe, ale nieoptymalne. Ma to sens, że kontener jest miejscem, w którym ujmujemy złożoność związaną z widokiem i wchodzimy w interakcję ze sklepem, ale:
    • Kontenery muszą być bardziej złożone niż głupie komponenty, ale nadal jest to jedna odpowiedzialność: zapewnienie powiązań między widokiem a stanem / sklepem. Twoja logika asynchroniczna to zupełnie inna sprawa.
    • Umieszczając go w kontenerze, blokowałbyś logikę asynchroniczną w jednym kontekście dla jednego widoku / trasy. Kiepski pomysł. Idealnie jest to wszystko wielokrotnego użytku i całkowicie oddzielone.
  • Czy jest inny moduł serwisowy? Zły pomysł: musisz wstrzyknąć dostęp do sklepu, który jest koszmarem związanym z utrzymaniem / testowalnością. Lepiej iść z ziarnem Redux i uzyskać dostęp do sklepu tylko przy użyciu dostarczonych interfejsów API / modeli.
  • Działania i Middleware, które je interpretują? Dlaczego nie?! Na początek jest to jedyna ważna opcja, którą pozostawiliśmy. :-) Bardziej logicznie, system akcji to logika wykonania oddzielona od sprzężenia, z której można korzystać z dowolnego miejsca. Ma dostęp do sklepu i może wysłać więcej akcji. Ma jeden obowiązek, który polega na organizowaniu przepływu kontroli i danych wokół aplikacji, i większość asynchronizacji pasuje właśnie do tego.
    • Co z twórcami akcji? Dlaczego nie po prostu wykonać tam asynchronizacji zamiast samych działań i oprogramowania pośredniego?
      • Po pierwsze, twórcy nie mają dostępu do sklepu, podobnie jak oprogramowanie pośrednie. Oznacza to, że nie możesz wysłać nowych akcji warunkowych, nie możesz czytać ze sklepu w celu skomponowania asynchronii itp.
      • Zachowaj więc złożoność w miejscu, które jest skomplikowane z konieczności, a wszystko inne proste. Twórcy mogą wtedy być prostymi, względnie czystymi funkcjami, które są łatwe do przetestowania.

Elementy kontenerowe - dlaczego nie? Ze względu na rolę, jaką odgrywają komponenty w React, kontener może działać jako klasa usługi, a już dostaje sklep za pośrednictwem DI (rekwizyty). Umieszczając go w kontenerze, zablokowałbyś logikę asynchroniczną w jednym kontekście dla jednego widoku / trasy - jak to możliwe? Komponent może mieć wiele instancji. Można go oddzielić od prezentacji, np. Za pomocą renderowania rekwizytów. Wydaje mi się, że odpowiedź mogłaby skorzystać jeszcze bardziej z krótkich przykładów, które to potwierdzają.
Estus Flask,

Uwielbiam tę odpowiedź!
Mauricio Avendaño

13

Aby odpowiedzieć na pytanie zadane na początku:

Dlaczego komponent kontenera nie może wywołać asynchronicznego interfejsu API, a następnie wywołać działania?

Pamiętaj, że te dokumenty dotyczą Redux, a nie Redux plus React. Sklepy Redux podłączone do komponentów React mogą robić dokładnie to, co mówisz, ale sklep Plain Jane Redux bez oprogramowania pośredniego nie przyjmuje argumentów dispatchoprócz zwykłych obiektów ol.

Bez oprogramowania pośredniego można oczywiście nadal

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

Ale jest to podobny przypadek, w którym asynchronia jest owinięta wokół Redux, a nie obsługiwana przez Redux. Oprogramowanie pośrednie pozwala więc na asynchronię poprzez modyfikację tego, co można przekazać bezpośrednio dispatch.


To powiedziawszy, duch waszej sugestii jest, jak sądzę, ważny. Z pewnością istnieją inne sposoby obsługi asynchronii w aplikacji Redux + React.

Jedną z zalet korzystania z oprogramowania pośredniego jest to, że możesz nadal używać kreatorów akcji bez normalnego martwienia się o to, jak są podłączeni. Na przykład przy użyciu redux-thunknapisany kod wyglądałby bardzo podobnie

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

który nie wygląda tak różnie od oryginału - jest tylko trochę przetasowany - i connectnie wie, że updateThingjest (lub musi być) asynchroniczny.

Jeśli chcesz także wspierać obietnice , obserwowalne , sagi lub zwariowanych niestandardowych i wysoce deklaratywnych twórców akcji, Redux może to zrobić, zmieniając to, co przekazujesz dispatch(czyli to, co powrócisz od twórców akcji). Nie connecttrzeba tłumić komponentów React (ani wywołań).


Radzisz, aby po prostu wysłać kolejne wydarzenie po zakończeniu akcji. To nie zadziała, gdy będziesz musiał pokazać alert () po zakończeniu akcji. Jednak obietnice zawarte w komponentach React działają. Obecnie polecam podejście Promises.
katamfetamina

8

OK, zacznijmy od tego, jak najpierw działa oprogramowanie pośrednie, które dość odpowiada na pytanie, to jest kod źródłowy funkcji pplyMiddleWare w Redux:

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

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

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

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

Spójrz na tę część, zobacz, jak nasza wysyłka stała się funkcją .

  ...
  getState: store.getState,
  dispatch: function dispatch(action) {
  return _dispatch(action);
}
  • Zauważ, że każde oprogramowanie pośrednie otrzyma funkcje dispatchi getStatejako nazwane argumenty.

OK, oto jak Redux-thunk jako jeden z najczęściej używanych programów pośrednich dla Redux przedstawia się:

Oprogramowanie pośrednie Redux Thunk pozwala pisać twórców akcji, którzy zwracają funkcję zamiast akcji. Thunk może być wykorzystany do opóźnienia wysyłki akcji lub wysyłki tylko wtedy, gdy spełniony jest określony warunek. Funkcja wewnętrzna odbiera metody store dispatch i getState jako parametry.

Jak więc widzisz, funkcja zwróci funkcję zamiast akcji, co oznacza, że ​​możesz poczekać i wywołać ją w dowolnym momencie, ponieważ jest to funkcja ...

Więc co, u licha, jest gromkie? Tak to zostało przedstawione w Wikipedii:

W programowaniu komputerowym thunk jest podprogramem używanym do wstrzykiwania dodatkowych obliczeń do innego podprogramu. Klocki są używane przede wszystkim do opóźniania obliczeń, dopóki nie są potrzebne, lub do wstawiania operacji na początku lub na końcu innego podprogramu. Mają wiele innych aplikacji do generowania kodu kompilatora i programowania modułowego.

Termin powstał jako żartobliwa pochodna „myślenia”.

Thunk to funkcja, która otacza wyrażenie, aby opóźnić jego ocenę.

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

Zobacz, jak prosta jest ta koncepcja i jak może pomóc Ci zarządzać działaniami asynchronicznymi ...

To jest coś, bez czego możesz żyć, ale pamiętaj, że w programowaniu zawsze są lepsze, schludniejsze i odpowiednie sposoby robienia rzeczy ...

Zastosuj redware oprogramowania pośredniego


1
Pierwszy raz na SO, nic nie czytałem. Ale podobał mi się post oglądający zdjęcie. Niesamowite, podpowiedź i przypomnienie.
Bhojendra Rauniyar

2

Saga Redux-saga jest najlepszym oprogramowaniem pośrednim w implementacji React-redux.

Np .: 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;

A potem 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()
       ]
    }

A potem action.js

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

A potem reder.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; 

A potem 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'));

spróbuj tego .. działa


3
Jest to poważna sprawa dla kogoś, kto chce tylko wywołać punkt końcowy interfejsu API, aby zwrócić encję lub listę encji. Polecasz: „po prostu zrób to ... potem to, potem to, potem jeszcze jedno, potem tamto, potem te inne rzeczy, a następnie kontynuuj, a potem ...”. Ale człowieku, to jest FRONTEND, wystarczy zadzwonić do BACKEND, aby dać nam dane gotowe do użycia na interfejsie. Jeśli jest to do zrobienia, coś jest nie tak, coś jest naprawdę nie tak i ktoś nie ubiega się obecnie KISS
zameb

Witaj, Użyj try-catch block dla wywołań API. Gdy interfejs API udzieli odpowiedzi, wywołaj typy akcji Reduktora.
SM Chinna

1
@zameb Możesz mieć rację, ale twoja skarga dotyczy samego Redux i wszystkich podsłuchanych przez niego prób, aby zmniejszyć złożoność.
jorisw

1

Są twórcy akcji synchronicznych, a następnie są twórcy akcji asynchronicznych.

Synchroniczny twórca akcji to taki, który gdy go nazwiemy, natychmiast zwraca obiekt Action ze wszystkimi odpowiednimi danymi dołączonymi do tego obiektu i gotowy do przetworzenia przez nasze reduktory.

Twórcy akcji asynchronicznych to takie, w których przygotowanie zajmie trochę czasu.

Z definicji, ilekroć masz kreatora akcji, który wysyła żądanie sieciowe, zawsze będzie się kwalifikował jako kreator akcji asynchronicznych.

Jeśli chcesz mieć asynchronicznych twórców akcji w aplikacji Redux, musisz zainstalować coś, co nazywa się oprogramowaniem pośrednim, które pozwoli ci radzić sobie z tymi asynchronicznymi twórcami akcji.

Możesz to sprawdzić w komunikacie o błędzie informującym, że używamy niestandardowego oprogramowania pośredniego do akcji asynchronicznych.

Czym jest oprogramowanie pośrednie i dlaczego potrzebujemy go do przepływu asynchronicznego w Redux?

W kontekście reduksowego oprogramowania pośredniego, takiego jak redux-thunk, oprogramowanie pośrednie pomaga nam radzić sobie z twórcami działań asynchronicznych, ponieważ jest to coś, czego Redux nie jest w stanie obsłużyć po wyjęciu z pudełka.

Dzięki oprogramowaniu pośredniczącemu zintegrowanemu z cyklem Redux nadal wzywamy twórców akcji, którzy zwrócą akcję, która zostanie wysłana, ale teraz, gdy wysyłamy akcję, zamiast wysyłać ją bezpośrednio do wszystkich naszych reduktorów, zamierzamy aby powiedzieć, że akcja zostanie wysłana przez różne oprogramowanie pośrednie wewnątrz aplikacji.

Wewnątrz jednej aplikacji Redux możemy mieć tyle lub mniej oprogramowania pośredniego, ile chcemy. W większości przypadków w projektach, nad którymi pracujemy, do naszego sklepu Redux zostanie podłączone jedno lub dwa oprogramowanie pośrednie.

Oprogramowanie pośrednie to zwykła funkcja JavaScript, która będzie wywoływana przy każdym wykonywanym przez nas działaniu. Wewnątrz tej funkcji oprogramowanie pośrednie ma możliwość zatrzymania wysyłania akcji do dowolnego reduktora, może modyfikować akcję lub po prostu zadzierać z akcją w dowolny sposób, który na przykład możemy stworzyć oprogramowanie pośredniczące, które loguje konsolę każdą akcję, którą wykonujesz tylko dla przyjemności oglądania.

Istnieje ogromna liczba oprogramowania pośredniego typu open source, które można zainstalować jako zależności w projekcie.

Nie jesteś ograniczony tylko do korzystania z oprogramowania pośredniego typu open source lub instalowania ich jako zależności. Możesz napisać własne niestandardowe oprogramowanie pośrednie i używać go w sklepie Redux.

Jednym z bardziej popularnych zastosowań oprogramowania pośredniego (i uzyskania odpowiedzi) jest praca z twórcami działań asynchronicznych, prawdopodobnie najpopularniejszym oprogramowaniem pośrednim jest redux-thunk i pomoc w radzeniu sobie z twórcami działań asynchronicznych.

Istnieje wiele innych rodzajów oprogramowania pośredniego, które również pomagają w radzeniu sobie z twórcami działań asynchronicznych.


1

Aby odpowiedzieć na pytanie:

Dlaczego komponent kontenera nie może wywołać asynchronicznego interfejsu API, a następnie wywołać działania?

Powiedziałbym z co najmniej dwóch powodów:

Pierwszym powodem jest rozdzielenie obaw, nie jest zadaniem action creatoroddzwanianie apii odzyskiwanie danych, musisz przekazać dwa argumenty swojemu action creator function, action typei payload.

Drugi powód jest taki, że redux storeczeka na zwykły obiekt z obowiązkowym typem akcji i opcjonalnie a payload(ale tutaj musisz również przekazać ładunek).

Kreator akcji powinien być prostym obiektem, takim jak poniżej:

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

I zadanie Redux-Thunk midlewaredo dispachewyniku swojego api calldo odpowiedniego action.


0

Podczas pracy w projekcie korporacyjnym istnieje wiele wymagań dostępnych w oprogramowaniu pośrednim, takich jak (saga) niedostępnych w prostym przepływie asynchronicznym, poniżej niektóre:

  • Uruchamianie żądania równolegle
  • Podejmowanie przyszłych działań bez konieczności oczekiwania
  • Nieblokujące połączenia Efekt wyścigu, najpierw przykład odbioru
  • reakcja na rozpoczęcie procesu Sekwencjonowanie zadań (pierwsze w pierwszym połączeniu)
  • Uspokajający
  • Anulowanie zadania Dynamiczne rozwiązywanie zadania.
  • Obsługa współbieżności Uruchamianie sagi poza redux redware.
  • Korzystanie z kanałów

Lista jest długa, wystarczy przejrzeć sekcję zaawansowaną w dokumentacji sagi

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.