Plusy / minusy używania redux-saga z generatorami ES6 vs redux-thunk z ASYNCHRONIĄ ES2017 /

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

Zanim się tym zajmę, chciałbym poznać plusy/minusy używania redux-saga zamiast poniższego podejścia, gdzie używam redux-thunk z asynchronią/oczekiwaniem.

Komponent może wyglądać tak, 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ą jak to:

// 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...
Author: Evaldas Buinauskas, 2016-01-21

7 answers

W redux-saga odpowiednikiem powyższego przykładu będzie

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ą, którą należy zauważyć, jest to, że wywołujemy funkcje api za pomocą formularza yield call(func, ...args). call nie wykonuje efektu, po prostu tworzy zwykły obiekt, taki jak {type: 'CALL', func, args}. Wykonanie jest delegowane do oprogramowania pośredniczącego redux-saga, które zajmuje się wykonaniem funkcji i wznowieniem generatora z jej wynikiem.

Główną zaletą jest to, że można przetestować generator poza Redux za pomocą prostego kontrole 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 })
)

Uwaga wyśmiewamy wynik wywołania api, po prostu wstrzykując wyśmiewane dane do metody next iteratora. Wyśmiewanie danych jest o wiele prostsze niż wyśmiewanie funkcji.

Drugą rzeczą, na którą należy zwrócić uwagę, jest wezwanie do yield take(ACTION). Thunks są wywoływane przez twórcę akcji przy każdej nowej akcji (np. LOGIN_REQUEST). tzn. akcje są nieustannie popychane do thunksa, a thunks nie ma kontroli, kiedy przestać je wykonywać.

W redux-saga, Generatory pociągnij następną akcję. tj. mają kontrolę kiedy słuchać jakiejś akcji, a kiedy nie. W powyższym przykładzie instrukcje przepływu są umieszczone wewnątrz pętli while(true), więc będzie ona nasłuchiwać każdej nadchodzącej akcji, która w pewien sposób naśladuje zachowanie pchania thunk.

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

  • Obsługa logowania użytkownika akcja

  • Po pierwszym pomyślnym zalogowaniu serwer zwraca token, który wygasa z pewnym opóźnieniem przechowywanym w polu expires_in. Będziemy musieli odświeżyć autoryzację w tle na każdą expires_in milisekundę

  • Należy wziąć pod uwagę, że w oczekiwaniu na wynik wywołań api (początkowego logowania lub odświeżania) użytkownik może wylogować się pomiędzy nimi.

Jak zaimplementowałbyś to z thunksem; jednocześnie zapewniając pełny zakres testów dla cały przepływ? Oto jak to może wyglądać z Sagami:

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 wymagania dotyczące współbieżności za pomocą race. Jeśli take(LOGOUT) wygra wyścig (tzn. użytkownik kliknął przycisk wylogowania). Wyścig automatycznie anuluje authAndRefreshTokenOnExpiry zadanie w tle. A jeśli {[14] } został zablokowany w środku call(authorize, {token}) połączenie zostanie również anulowane. Anulowanie propaguje się automatycznie w dół.

Można znaleźć runnable demo powyższego przepływu

 389
Author: Yassine Elouafi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-08-11 15:50:25

Dodam moje doświadczenie z wykorzystaniem sagi w systemie produkcyjnym do dość dokładnej odpowiedzi autora biblioteki.

Pro (using saga):

  • Testowalność. Bardzo łatwo jest przetestować sagi, ponieważ call () zwraca czysty obiekt. Testowanie thunksa zwykle wymaga włączenia mockStore do testu.

  • Redux - saga zawiera wiele przydatnych funkcji pomocniczych dotyczących zadań. Wydaje mi się, że koncepcja sagi polega na stworzeniu pewnego rodzaju tła worker / thread dla Twojej aplikacji, która działa jak brakujący element w architekturze React redux (actioncreatory i reduktory muszą być czystymi funkcjami.), Co prowadzi do następnego punktu.

  • Sagi oferują niezależne miejsce do obsługi wszystkich skutków ubocznych. Zazwyczaj łatwiej jest modyfikować i zarządzać niż thunk akcji w moim doświadczeniu.

Con:

  • Składnia generatora.

  • Wiele pojęć do nauki.

  • Stabilność API. Wydaje się, że redux-saga jest nadal dodawanie funkcji(np. kanały?), a społeczność nie jest tak duża. Istnieje obawa, czy biblioteka pewnego dnia dokona nie zgodnej wstecz aktualizacji.

 78
Author: yjcxy12,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-06-22 16:20:05

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

Sagi świetnie sprawdzają się:

  • nie musisz naśladować funkcji owiniętych efektami
  • dlatego testy są czyste, czytelne i łatwe do napisania
  • podczas używania sagi twórcy akcji zwracają najczęściej zwykłe literały obiektów. Łatwiej jest również przetestować i potwierdzić w przeciwieństwie do obietnic thunka.
Sagi są potężniejsze. Wszystko, co możesz zrobić w jednym thunk ' s Action creator można również zrobić w jednej sadze, ale nie odwrotnie (a przynajmniej nie łatwo). Na przykład:
  • oczekiwanie na wysłanie akcji/akcji (take)
  • anuluj istniejącą rutynę (cancel, takeLatest, race)
  • wiele procedur może słuchać tej samej akcji (take, takeEvery, ...)

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

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

Sagi są wielkim i potężnym narzędziem. Jednak z mocą przychodzi odpowiedzialność. Gdy Twoja aplikacja rośnie, możesz łatwo stracić, zastanawiając się, kto czeka na wysłanie akcji lub co się dzieje, gdy jakaś akcja jest wysyłana. Z drugiej strony thunk jest prostszy i łatwiejszy do rozumowania. Wybór jednego lub drugiego zależy od wielu aspektów, takich jak typ i rozmiar projektu, jakie rodzaje efektów ubocznych musi obsługiwać Twój projekt lub preferencje zespołu deweloperskiego. W każdym razie po prostu zachowaj prostą i przewidywalną aplikację.

 18
Author: madox2,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-01-29 08:58:21

Po przejrzeniu kilku różnych dużych projektów React/Redux w moim doświadczeniu, Saga zapewniają programistom bardziej ustrukturyzowany sposób pisania kodu, który jest o wiele łatwiejszy do przetestowania i trudniejszy do pomyłki.

Tak, to trochę dziwne, ale większość programistów ma dość zrozumienia tego w jeden dzień. Zawsze mówię ludziom, aby nie martwili się o to, co yield robi na początek i że jak napiszesz kilka testów to przyjdzie do ciebie.

Widziałem kilka projektów gdzie thunksy zostały potraktowane tak, jakby były kontrolerami z MVC patten i to szybko staje się niezniszczalnym bałaganem.

Moja rada to użycie sagi, w której potrzebujesz wyzwalacza typu B, związanego z pojedynczym wydarzeniem. W przypadku wszystkiego, co mogłoby przeciąć wiele akcji, łatwiej jest napisać oprogramowanie pośredniczące klienta i użyć meta właściwości akcji FSA do jej uruchomienia.

 0
Author: David Bradshaw,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-14 21:04:03

Oto projekt, który łączy najlepsze części (plusy) zarówno redux-saga jak i redux-thunk: możesz poradzić sobie ze wszystkimi efektami ubocznymi na sagach, otrzymując obietnicę przez dispatching odpowiednią akcję: 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)
    })
  }
}
 -1
Author: Diego Haz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 03:39:00

Łatwiejszym sposobem jest użycie redux-auto .

Z dokumentu

Redux-auto naprawił ten problem asynchroniczny, pozwalając na utworzenie funkcji "action", która zwraca obietnicę. Aby towarzyszyć" domyślnej " logiki działania funkcji.

  1. nie ma potrzeby stosowania innego oprogramowania pośredniczącego Redux asynchronicznego. np. thunk, promise-middleware, saga
  2. łatwo pozwala przekazać obietnicę do redux i mieć ją za ty
  3. pozwala na wspólne lokalizowanie połączeń zewnętrznych z miejscem, w którym zostaną przekształcone
  4. nazwanie pliku " init.js " wywoła go raz przy starcie aplikacji. Jest to dobre do ładowania danych z serwera przy starcie

Chodzi o to, aby każda akcja była umieszczona w konkretnym pliku . współlokowanie wywołania serwera w pliku z funkcjami "oczekujące", "spełnione" i "odrzucone". To sprawia, że obsługa obietnic jest bardzo łatwa.

To również automatycznie dołącza obiekt pomocniczy (zwany "async") do prototypu twojego stanu, umożliwiając śledzenie w interfejsie użytkownika żądanych przejść.

 -1
Author: codemeasandwich,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-24 13:25:10

Jedna szybka nuta. Generatory można anulować, asynchroniczne / oczekujące-nie. Więc dla przykładu z pytania, to naprawdę nie ma sensu, co wybrać. Ale dla bardziej skomplikowanych przepływów czasami nie ma lepszego rozwiązania niż korzystanie z generatorów.

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

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

 -2
Author: Dmitriy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-14 22:11:29