Plusy / minusy korzystania z redux-sagi z generatorami ES6 vs redux-thunk z ES2017 async / czekają

488

Obecnie dużo mówi się o najnowszym dzieciaku w mieście Redux , redux-saga / redux-saga . Wykorzystuje funkcje generatora do nasłuchiwania / wysyłania akcji.

Zanim owinę głowę, chciałbym poznać zalety / wady korzystania redux-sagazamiast podejścia poniżej, w którym korzystam redux-thunkz asynchronizacji / czekania.

Składnik może wyglądać tak: wywołać działania jak zwykle.

import { login } from 'redux/auth';

class LoginForm extends Component {

  onClick(e) {
    e.preventDefault();
    const { user, pass } = this.refs;
    this.props.dispatch(login(user.value, pass.value));
  }

  render() {
    return (<div>
        <input type="text" ref="user" />
        <input type="password" ref="pass" />
        <button onClick={::this.onClick}>Sign In</button>
    </div>);
  } 
}

export default connect((state) => ({}))(LoginForm);

Wtedy moje działania wyglądają mniej więcej tak:

// auth.js

import request from 'axios';
import { loadUserData } from './user';

// define constants
// define initial state
// export default reducer

export const login = (user, pass) => async (dispatch) => {
    try {
        dispatch({ type: LOGIN_REQUEST });
        let { data } = await request.post('/login', { user, pass });
        await dispatch(loadUserData(data.uid));
        dispatch({ type: LOGIN_SUCCESS, data });
    } catch(error) {
        dispatch({ type: LOGIN_ERROR, error });
    }
}

// more actions...

// user.js

import request from 'axios';

// define constants
// define initial state
// export default reducer

export const loadUserData = (uid) => async (dispatch) => {
    try {
        dispatch({ type: USERDATA_REQUEST });
        let { data } = await request.get(`/users/${uid}`);
        dispatch({ type: USERDATA_SUCCESS, data });
    } catch(error) {
        dispatch({ type: USERDATA_ERROR, error });
    }
}

// more actions...
hampusohlsson
źródło
6
Zobacz także moją odpowiedź porównującą redux-thunk do redux-saga tutaj: stackoverflow.com/a/34623840/82609
Sebastien Lorber
22
Co robisz ::przedtem this.onClick?
Downhillski
37
@ZhenyangHua to skrót do powiązania funkcji z obiektem ( this), alias this.onClick = this.onClick.bind(this). Dłuższa forma jest zwykle zalecana w konstruktorze, ponieważ skrót jest ponownie powiązany na każdym renderowaniu.
hampusohlsson
7
Widzę. dzięki! Widzę, że ludzie bind()często używają thistej funkcji, ale zacząłem używać () => method()teraz.
Downhillski
2
@Hosar Użyłem redux i redux-sagi przez jakiś czas w produkcji, ale po kilku miesiącach migrowałem do MobX, ponieważ mniej narzutów
hampusohlsson

Odpowiedzi:

461

W redux-saga byłby to odpowiednik powyższego przykładu

export function* loginSaga() {
  while(true) {
    const { user, pass } = yield take(LOGIN_REQUEST)
    try {
      let { data } = yield call(request.post, '/login', { user, pass });
      yield fork(loadUserData, data.uid);
      yield put({ type: LOGIN_SUCCESS, data });
    } catch(error) {
      yield put({ type: LOGIN_ERROR, error });
    }  
  }
}

export function* loadUserData(uid) {
  try {
    yield put({ type: USERDATA_REQUEST });
    let { data } = yield call(request.get, `/users/${uid}`);
    yield put({ type: USERDATA_SUCCESS, data });
  } catch(error) {
    yield put({ type: USERDATA_ERROR, error });
  }
}

Pierwszą rzeczą, na którą należy zwrócić uwagę, jest to, że wywołujemy funkcje API za pomocą formularza yield call(func, ...args). callnie wykonuje efektu, po prostu tworzy zwykły obiekt podobny do {type: 'CALL', func, args}. Wykonanie jest delegowane do oprogramowania pośredniego redux-saga, które dba o wykonanie funkcji i wznowienie działania generatora wraz z wynikiem.

Główną zaletą jest to, że można przetestować generator poza Redux za pomocą prostych kontroli równości

const iterator = loginSaga()

assert.deepEqual(iterator.next().value, take(LOGIN_REQUEST))

// resume the generator with some dummy action
const mockAction = {user: '...', pass: '...'}
assert.deepEqual(
  iterator.next(mockAction).value, 
  call(request.post, '/login', mockAction)
)

// simulate an error result
const mockError = 'invalid user/password'
assert.deepEqual(
  iterator.throw(mockError).value, 
  put({ type: LOGIN_ERROR, error: mockError })
)

Zauważ, że wyśmiewamy wynik wywołania interfejsu API, po prostu wstrzykując wyszydzone dane do nextmetody iteratora. Wyśmiewanie danych jest znacznie prostsze niż wyśmiewanie funkcji.

Drugą rzeczą, na którą należy zwrócić uwagę, jest połączenie z yield take(ACTION). Twardy są wywoływane przez twórcę akcji przy każdej nowej akcji (np LOGIN_REQUEST.). tzn. akcje są nieustannie wypychane na kolce, a kolce nie mają kontroli nad tym, kiedy przestać je wykonywać.

W Redux-sagi, generatory wyciągnąć następną akcję. tzn. mają kontrolę nad tym, kiedy słuchać jakiejś akcji, a kiedy nie. W powyższym przykładzie instrukcje przepływu są umieszczone w while(true)pętli, więc będzie nasłuchiwał każdej przychodzącej akcji, która nieco naśladuje zachowanie polegające na pchaniu.

Podejście pull umożliwia wdrożenie złożonych przepływów sterowania. Załóżmy na przykład, że chcemy dodać następujące wymagania

  • Obsługuj akcję użytkownika LOGOUT

  • po pierwszym udanym logowaniu serwer zwraca token, który wygasa z pewnym opóźnieniem przechowywanym w expires_inpolu. Będziemy musieli odświeżać autoryzację w tle co expires_inmilisekundy

  • Weź pod uwagę, że podczas oczekiwania na wynik wywołań interfejsu API (początkowe logowanie lub odświeżenie) użytkownik może się wylogować pomiędzy nimi.

Jak byś to wdrożył za pomocą thunks; zapewniając jednocześnie pełne pokrycie testowe dla całego przepływu? Oto, jak może to wyglądać w Sagas:

function* authorize(credentials) {
  const token = yield call(api.authorize, credentials)
  yield put( login.success(token) )
  return token
}

function* authAndRefreshTokenOnExpiry(name, password) {
  let token = yield call(authorize, {name, password})
  while(true) {
    yield call(delay, token.expires_in)
    token = yield call(authorize, {token})
  }
}

function* watchAuth() {
  while(true) {
    try {
      const {name, password} = yield take(LOGIN_REQUEST)

      yield race([
        take(LOGOUT),
        call(authAndRefreshTokenOnExpiry, name, password)
      ])

      // user logged out, next while iteration will wait for the
      // next LOGIN_REQUEST action

    } catch(error) {
      yield put( login.error(error) )
    }
  }
}

W powyższym przykładzie wyrażamy nasze wymaganie dotyczące współbieżności za pomocą race. Jeśli take(LOGOUT)wygrywa wyścig (tj. Użytkownik kliknął przycisk wylogowania). Wyścig automatycznie anuluje authAndRefreshTokenOnExpiryzadanie w tle. A jeśli authAndRefreshTokenOnExpiryzostał zablokowany w środkucall(authorize, {token}) połączenia, zostanie również anulowany. Anulowanie następuje automatycznie w dół.

Możesz znaleźć działające demo powyższego przepływu

Yassine Elouafi
źródło
@yassine skąd pochodzi delayfunkcja? Ach, znalazłem to: github.com/yelouafi/redux-saga/blob/…
phil
122
redux-thunkKod jest dość czytelny i self-wyjaśnił. Ale redux-sagasjedno jest bardzo nieczytelny, głównie z powodu tych czasownika-jak funkcje: call, fork, take, put...
syg
11
@syg, zgadzam się, że call, fork, take i put mogą być bardziej semantycznie przyjazne. Jednak to te funkcje podobne do czasownika umożliwiają testowanie wszystkich efektów ubocznych.
Downhillski
3
@syg nadal funkcja z tymi dziwnymi funkcjami czasowników jest bardziej czytelna niż funkcja z łańcuchem głębokich obietnic
Yasser Sinjab
3
te „dziwne” czasowniki pomagają ci także zrozumieć stosunek sagi do wiadomości wychodzących z redux. można wziąć typy wiadomości z Redux - często do wywołania kolejnej iteracji, można położyć nowe wiadomości z powrotem na nadawanie wynik swojej efekt uboczny.
worc,
104

Dodam swoje doświadczenie w stosowaniu sagi w systemie produkcyjnym, oprócz dość dokładnej odpowiedzi autora biblioteki.

Pro (za pomocą saga):

  • Testowalność Testowanie sag jest bardzo łatwe, ponieważ call () zwraca czysty obiekt. Testowanie thunks zwykle wymaga uwzględnienia mockStore w teście.

  • Redux-saga zawiera wiele przydatnych funkcji pomocniczych dotyczących zadań. Wydaje mi się, że koncepcja sagi polega na stworzeniu pewnego rodzaju elementu roboczego / wątku w tle dla twojej aplikacji, który działa jako brakujący element w architekturze redux reagowania (ActionCreators i reduktory muszą być czystymi funkcjami). Co prowadzi do następnego punktu.

  • Sagi oferują niezależne miejsce do radzenia sobie ze wszystkimi skutkami ubocznymi. Z mojego doświadczenia wynika, że ​​zazwyczaj łatwiej jest modyfikować i zarządzać niż piorunujące akcje.

Kon:

  • Składnia generatora.

  • Wiele pojęć do nauczenia się.

  • Stabilność API. Wygląda na to, że redux-saga wciąż dodaje funkcje (np. Kanały?), A społeczność nie jest tak duża. Istnieje obawa, że ​​biblioteka dokona pewnego dnia niekompatybilnej aktualizacji.

yjcxy12
źródło
9
Po prostu chcę skomentować, twórca akcji nie musi być czystą funkcją, o czym sam Dan wielokrotnie twierdził.
Marson Mao
14
Na razie bardzo zalecane są sagi redux, ponieważ ich wykorzystanie i społeczność się powiększyła. Ponadto interfejs API stał się bardziej dojrzały. Rozważ usunięcie Con API stabilityjako aktualizacji, aby odzwierciedlić obecną sytuację.
Denialos,
1
saga ma więcej początków niż thunk, a jej ostatnie zatwierdzenie następuje także po thunk
amorenew
2
Tak, redux-saga FWIW ma teraz 12 tys. Gwiazdek, redux-thunk ma 8 tys.
Brian Burns,
3
Dodam jeszcze jedno wyzwanie sag, polegające na tym, że sagi domyślnie są całkowicie oddzielone od akcji i twórców akcji. Podczas gdy Thunks bezpośrednio łączą twórców akcji z ich efektami ubocznymi, sagi pozostawiają twórców akcji całkowicie oddzielonych od sag, które ich słuchają. Ma to zalety techniczne, ale może znacznie utrudnić śledzenie kodu i zatrzeć niektóre koncepcje jednokierunkowe.
theaceofthespade
33

Chciałbym tylko dodać kilka uwag z mojego osobistego doświadczenia (używając zarówno sag, jak i thunk):

Sagi świetnie się sprawdzają:

  • Nie musisz wyśmiewać funkcji opakowanych efektami
  • Dlatego testy są czyste, czytelne i łatwe do napisania
  • Podczas korzystania z sag twórcy akcji zwracają głównie literały zwykłego obiektu. Łatwiej jest też testować i dochodzić w przeciwieństwie do obietnic Thunk.

Sagi są silniejsze. Wszystko, co możesz zrobić w kreatorze akcji jednego Thunka, możesz także zrobić w jednej sadze, ale nie odwrotnie (a przynajmniej niełatwo). Na przykład:

  • czekać na akcję / akcje, które zostaną wysłane (take )
  • anuluj istniejącą procedurę ( cancel,takeLatest ,race )
  • wiele procedur może nasłuchiwać tej samej akcji ( take,takeEvery ...)

Sagas oferuje również inne przydatne funkcje, które uogólniają niektóre popularne wzorce aplikacji:

  • channels słuchać zewnętrznych źródeł zdarzeń (np. websockets)
  • model widelca ( fork,spawn )
  • przepustnica
  • ...

Sagi to świetne i potężne narzędzie. Jednak wraz z mocą wiąże się odpowiedzialność. Gdy Twoja aplikacja rośnie, możesz łatwo zgubić się, zastanawiając się, kto czeka na akcję do wysłania lub co się stanie, gdy akcja zostanie wysłana. Z drugiej strony thunk jest prostszy i łatwiejszy do uzasadnienia. Wybór jednego lub drugiego zależy od wielu aspektów, takich jak rodzaj i rozmiar projektu, jakie rodzaje skutków ubocznych Twój projekt musi obsłużyć lub preferencje zespołu programistów. W każdym razie po prostu utrzymaj swoją aplikację prostą i przewidywalną.

madox2
źródło
8

Po prostu osobiste doświadczenie:

  1. Jeśli chodzi o styl kodowania i czytelność, jedną z najważniejszych zalet korzystania z redux-sagi w przeszłości jest unikanie piekła zwrotnego w redux-thunk - nie trzeba już używać wielu zagnieżdżeń. Ale teraz, gdy popularność async / czekaj na thunk, można również napisać kod asynchroniczny w stylu synchronizacji, używając redux-thunk, co można uznać za poprawę w myśleniu redux.

  2. Podczas korzystania z redux-sagi może być konieczne napisanie o wiele więcej kodu, szczególnie w maszynopisie. Na przykład, jeśli chce się wdrożyć funkcję asynchronizacji pobierania, obsługę danych i błędów można wykonać bezpośrednio w jednej jednostce Thunk w pliku Action.js za pomocą jednej akcji FETCH. Ale w redux-saga może być konieczne zdefiniowanie akcji FETCH_START, FETCH_SUCCESS i FETCH_FAILURE oraz wszystkich powiązanych kontroli typu, ponieważ jedną z funkcji w redux-sadze jest użycie tego rodzaju bogatego mechanizmu „tokenów” do tworzenia efektów i instruowania sklep redux do łatwego testowania. Oczywiście można napisać sagę bez użycia tych działań, ale to by sprawiło, że byłaby podobna do bzdury.

  3. Jeśli chodzi o strukturę plików, saga redux wydaje się być bardziej wyraźna w wielu przypadkach. Można łatwo znaleźć kod asynchroniczny w każdym pliku sagas.ts, ale w redux-thunk trzeba by go zobaczyć w działaniach.

  4. Łatwe testowanie może być kolejną ważoną funkcją w redux-sadze. To jest naprawdę wygodne. Ale jedną rzeczą, którą należy wyjaśnić, jest to, że test „wywołania” redux-saga nie wykonałby rzeczywistego wywołania API podczas testowania, dlatego należałoby określić przykładowy wynik dla kroków, które mogą go wykorzystać po wywołaniu API. Dlatego przed napisaniem redagi-sagi lepiej byłoby dokładnie zaplanować sagę i odpowiadające jej sagas.spec.ts.

  5. Redux-saga zapewnia również wiele zaawansowanych funkcji, takich jak równoległe uruchamianie zadań, pomocniki współbieżności, takie jak takeLatest / takeEvery, fork / spawn, które są znacznie potężniejsze niż thunks.

Podsumowując, osobiście chciałbym powiedzieć: w wielu normalnych przypadkach i małych i średnich aplikacjach używaj asynchronicznego / oczekującego stylu redux-thunk. Pozwoliłoby to zaoszczędzić wiele kodów / akcji / typedefs i nie musiałbyś przełączać wielu różnych sagas.ts i utrzymywać określonego drzewa sagas. Ale jeśli tworzysz dużą aplikację o bardzo złożonej logice asynchronicznej i potrzebujesz takich funkcji, jak współbieżność / wzorzec równoległy lub masz duże zapotrzebowanie na testowanie i konserwację (szczególnie w rozwoju opartym na testach), sagi redux prawdopodobnie uratują ci życie .

W każdym razie saga redux nie jest trudniejsza i bardziej złożona niż sama redux i nie ma tak zwanej krzywej uczenia się, ponieważ ma dobrze ograniczone podstawowe koncepcje i interfejsy API. Poświęcenie niewielkiej ilości czasu na naukę sagi redux może przynieść korzyści w przyszłości.

Jonathan
źródło
5

Po zapoznaniu się z kilkoma różnymi projektami React / Redux na dużą skalę z mojego doświadczenia, Sagas zapewnia programistom bardziej uporządkowany sposób pisania kodu, który jest znacznie łatwiejszy do przetestowania i trudniejszy do popełnienia błędu.

Tak, to trochę dziwne na początek, ale większość deweloperów ma dość zrozumienia tego w ciągu jednego dnia. Zawsze mówię ludziom, żeby się nie martwili o coyield zacząć, i że po napisaniu kilku testów przyjdzie do ciebie.

Widziałem kilka projektów, w których gromady traktowano tak, jakby były kontrolerami z modelu MVC, a to szybko staje się nie do utrzymania.

Radzę używać Sag, gdzie potrzebujesz A wyzwala rzeczy typu B odnoszące się do pojedynczego zdarzenia. W przypadku czegokolwiek, co mogłoby przecinać wiele akcji, uważam, że łatwiej jest napisać oprogramowanie pośrednie klienta i użyć właściwości meta akcji FSA do jej uruchomienia.

David Bradshaw
źródło
2

Thunks vs. Sagas

Redux-Thunk i Redux-Saga różnią się pod kilkoma ważnymi względami, obie są bibliotekami oprogramowania pośredniego dla Redux (oprogramowanie pośrednie Redux to kod, który przechwytuje działania przychodzące do sklepu za pomocą metody dispatch ()).

Akcja może być dosłownie czymkolwiek, ale jeśli przestrzegasz najlepszych praktyk, akcja jest zwykłym obiektem javascript z polem typu i opcjonalnymi polami ładunku, meta i błędów. na przykład

const loginRequest = {
    type: 'LOGIN_REQUEST',
    payload: {
        name: 'admin',
        password: '123',
    }, };

Redux-Thunk

Oprócz wysyłania standardowych działań Redux-Thunkoprogramowanie pośrednie umożliwia wysyłanie specjalnych funkcji, zwanychthunks .

Thunks (w Redux) ogólnie mają następującą strukturę:

export const thunkName =
   parameters =>
        (dispatch, getState) => {
            // Your application logic goes here
        };

Oznacza to, że a thunkjest funkcją, która (opcjonalnie) przyjmuje niektóre parametry i zwraca inną funkcję. Funkcja wewnętrzna przyjmuje funkcję dispatch functioni getState- obie będą dostarczane przez Redux-Thunkoprogramowanie pośrednie.

Redux-Saga

Redux-Sagaoprogramowanie pośrednie pozwala wyrazić złożoną logikę aplikacji jako czyste funkcje zwane sagami. Czyste funkcje są pożądane z punktu widzenia testowania, ponieważ są przewidywalne i powtarzalne, co czyni je stosunkowo łatwymi do testowania.

Sagi są realizowane za pomocą specjalnych funkcji zwanych funkcjami generatora. Są to nowe funkcje ES6 JavaScript. Zasadniczo wykonanie wskakuje i wychodzi z generatora wszędzie tam, gdzie widać instrukcję dochodu. Pomyśl o yieldstwierdzeniu, które powoduje zatrzymanie generatora i zwrócenie uzyskanej wartości. Później osoba dzwoniąca może wznowić działanie generatora na wyciągu poyield .

Funkcja generatora to taka zdefiniowana w ten sposób. Zwróć uwagę na gwiazdkę po słowie kluczowym funkcji.

function* mySaga() {
    // ...
}

Po zarejestrowaniu sagi logowania Redux-Saga. Ale wtedy yieldprzyjęcie pierwszego wiersza zatrzyma sagę, dopóki akcja z typem nie 'LOGIN_REQUEST'zostanie wysłana do sklepu. Gdy to nastąpi, wykonywanie będzie kontynuowane.

Aby uzyskać więcej informacji, zobacz ten artykuł .

Mselmi Ali
źródło
1

Jedna szybka uwaga. Generatory można anulować, asynchronicznie / oczekuj - nie. Na przykład z pytania tak naprawdę nie ma sensu, co wybrać. Jednak w przypadku bardziej skomplikowanych przepływów czasami nie ma lepszego rozwiązania niż użycie generatorów.

Innym pomysłem może być użycie generatorów z redux-thunk, ale dla mnie wygląda to na próbę wynalezienia roweru z kwadratowymi kołami.

I oczywiście generatory są łatwiejsze do przetestowania.

Dmitriy
źródło
0

Oto projekt, który łączy w sobie najlepsze elementy (plusy) obu redux-sagai redux-thunk: można obsługiwać wszystkie skutki uboczne na sag podczas uzyskiwania przyrzeczenia przez dispatchingodpowiedniego działania: https://github.com/diegohaz/redux-saga-thunk

class MyComponent extends React.Component {
  componentWillMount() {
    // `doSomething` dispatches an action which is handled by some saga
    this.props.doSomething().then((detail) => {
      console.log('Yaay!', detail)
    }).catch((error) => {
      console.log('Oops!', error)
    })
  }
}
Diego Haz
źródło
1
używanie then()wewnątrz komponentu React jest sprzeczne z paradygmatem. Powinieneś poradzić sobie ze zmienionym stanem, componentDidUpdatezamiast czekać na rozwiązanie obietnicy.
3
@ Maxincredible52 Nie dotyczy to renderowania po stronie serwera.
Diego Haz
Z mojego doświadczenia wynika, że ​​punkt Maxa nadal odnosi się do renderowania po stronie serwera. Prawdopodobnie powinno to być obsługiwane gdzieś w warstwie routingu.
ThinkingInBits,
3
@ Maxincredible52 dlaczego jest to sprzeczne z paradygmatem, gdzie to przeczytałeś? Zwykle robię podobnie do @Diego Haz, ale robię to w componentDidMount (zgodnie z dokumentami React, połączenia sieciowe powinny być tam najlepiej wykonywane), więc mamycomponentDidlMount() { this.props.doSomething().then((detail) => { this.setState({isReady: true})} }
user3711421,
0

Łatwiejszym sposobem jest użycie redux-auto .

z dokumantacji

redux-auto naprawił ten asynchroniczny problem, po prostu umożliwiając utworzenie funkcji „akcji”, która zwraca obietnicę. Towarzyszy twojej „domyślnej” logice działania funkcji.

  1. Nie ma potrzeby używania innego oprogramowania pośredniego asynchronicznego Redux. np. thunk, obietnica-middleware, saga
  2. Łatwo pozwala ci przekazać obietnicę do redux i zarządzać nią za ciebie
  3. Pozwala na lokalizację zewnętrznych połączeń serwisowych z miejscem, w którym zostaną przekształcone
  4. Nazewnictwo pliku „init.js” wywoła go raz podczas uruchamiania aplikacji. Jest to dobre do ładowania danych z serwera na początku

Chodzi o to, aby każda akcja znajdowała się w określonym pliku . kolokacja wywołania serwera w pliku z funkcjami reduktora dla „oczekujących”, „spełnionych” i „odrzuconych”. To sprawia, że ​​obsługa obietnic jest bardzo łatwa.

Automatycznie dołącza również obiekt pomocnika (zwany „asynchronizacją”) do prototypu stanu, umożliwiając śledzenie w interfejsie użytkownika żądanych przejść.

codemeasandwich
źródło
2
Zrobiłem +1, nawet jeśli jest to nieistotna odpowiedź, ponieważ należy również rozważyć inne rozwiązania
poniedziałek
12
Myślę, że są, ponieważ nie ujawnił, że jest autorem projektu
jreptak