ES6ジェネレーターでredux-sagaを使用する場合と、ES2017 async/awaitでredux-thunkを使用する場合の長所と短所 質問する

ES6ジェネレーターでredux-sagaを使用する場合と、ES2017 async/awaitでredux-thunkを使用する場合の長所と短所 質問する

現在、リダックスタウンの最新キッズについて多くの話題があります。リダックスサーガ/リダックスサーガアクションのリッスン/ディスパッチにはジェネレータ関数を使用します。

それを理解する前に、async/await をredux-saga使用している以下のアプローチの代わりにを使用することの長所と短所を知りたいと思います。redux-thunk

コンポーネントは次のようになります。通常どおりアクションをディスパッチします。

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

私の行動は次のようになります。

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

ベストアンサー1

redux-sagaでは、上記の例と同等のものは次のようになります。

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

最初に気づくのは、 という形式を使用して API 関数を呼び出していることですyield call(func, ...args)callは効果を実行せず、 のような単純なオブジェクトを作成するだけです{type: 'CALL', func, args}。 実行は redux-saga ミドルウェアに委任され、関数の実行とその結果によるジェネレーターの再開が行われます。

主な利点は、単純な等価性チェックを使用してReduxの外でジェネレータをテストできることです。

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

モックされたデータをイテレータのメソッドに挿入するだけで、API 呼び出しの結果をモックしていることに注意してくださいnext。データのモックは関数のモックよりもはるかに簡単です。

2 番目に注意すべき点は、 の呼び出しですyield take(ACTION)。サンクは、新しいアクションごとにアクション作成者によって呼び出されます (例LOGIN_REQUEST)。つまり、アクションは継続的にサンクにプッシュされ、サンクはそれらのアクションの処理を停止するタイミングを制御できません。

redux-saga では、ジェネレーターが次のアクションをプルします。つまり、ジェネレーターは、いつアクションをリッスンするか、いつリッスンしないかを制御できます。上記の例では、フロー命令がwhile(true)ループ内に配置されているため、各着信アクションをリッスンします。これは、サンクのプッシュ動作をある程度模倣しています。

プルアプローチでは複雑な制御フローを実装できます。例えば、次の要件を追加したいとします。

  • ログアウトユーザーアクションを処理する

  • 最初のログインが成功すると、サーバーはフィールドに格納された一定時間後に期限切れになるトークンを返します。1ミリ秒expires_inごとにバックグラウンドで認証を更新する必要があります。expires_in

  • API 呼び出しの結果を待機しているとき (最初のログインまたは更新)、ユーザーは途中でログアウトする可能性があることに注意してください。

フロー全体の完全なテスト カバレッジを提供しながら、これをサンクで実装するにはどうすればよいでしょうか。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) )
    }
  }
}

上記の例では、同時実行要件を を使用して表現していますrace。 がtake(LOGOUT)競争に勝った場合 (つまり、ユーザーがログアウト ボタンをクリックした場合)、競争によりバックグラウンド タスクが自動的にキャンセルされます。 が呼び出しの途中でブロックされたauthAndRefreshTokenOnExpiry場合も、キャンセルされます。キャンセルは自動的に下方に伝播します。authAndRefreshTokenOnExpirycall(authorize, {token})

あなたは見つけることができます上記フローの実行可能なデモ

おすすめ記事