1. ホーム
  2. javascript

[解決済み] ES6 ジェネレータで redux-saga を使用する利点/欠点と ES2017 async/await で redux-thunk を使用する利点/欠点

2022-03-17 15:08:34

質問

今、redux townの最新の子供が話題になっていますね。 レデュックス・サーガ/REDUX-SAGA . これは、アクションをリッスン/ディスパッチするためのジェネレータ関数を使用します。

を使うことの長所と短所を教えてください。 redux-saga を使用する以下のアプローチではなく redux-thunk をasync/awaitで使用します。

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

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

解決方法は?

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

モックされたデータを単純に next メソッドを使用します。データをモックするのは、関数をモックするよりもずっと簡単です。

次に注目すべきは yield take(ACTION) . サンクは、アクションの作成者が新しいアクションのたびに呼び出します (例. LOGIN_REQUEST つまり、アクションは継続的に プッシュ をサンクに送り、サンクはいつそのアクションの処理をやめるかをコントロールできない。

redux-sagaでは、ジェネレータ 引っ張る つまり、あるアクションをいつリッスンするか、しないかをコントロールすることができます。上の例では、フロー指示は while(true) ループに入るので、アクションが来るたびにリッスンすることになり、サンクのプッシュ動作に多少似ています。

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

  • LOGOUT ユーザーアクションの処理

  • ログインに成功すると、サーバーはトークンを返し、その期限は 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 バックグラウンドタスクです。そして、もし authAndRefreshTokenOnExpiry の途中でブロックされました。 call(authorize, {token}) の呼び出しもキャンセルされます。キャンセルは自動的に下に伝搬します。

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