microsoft / redux-dynamic-modules

Modularize Redux by dynamically loading reducers and middlewares.
https://redux-dynamic-modules.js.org
MIT License
1.07k stars 116 forks source link

How to Run Watcher Sagas? #100

Closed ziongh closed 5 years ago

ziongh commented 5 years ago

I'm Trying to define a Saga to control Authentication Login Flow:

// Actions
export const actions = {
  LOGIN_REQUEST: "USERS_LOGIN_REQUEST",
  LOGIN_SUCCESS: "USERS_LOGIN_SUCCESS",
  LOGIN_FAILURE: "USERS_LOGIN_FAILURE",
  LOGIN_CANCELLED: "USERS_LOGIN_CANCELLED",
  LOGOUT: "USERS_LOGOUT",
  loginUserAction: (user) => {
    return {
      type: actions.LOGIN_REQUEST,
      user
    }
  },
  logoutAction: () => {
    return {
      type: actions.LOGOUT
    }
  }
};
// Reducer
function authReducer(state = initialState, action) {
  return produce(state || {}, draft => {
    switch (action.type) {
      case actions.LOGIN_REQUEST:
        draft.loggingIn = true;
        draft.user = action.user;
        break;
      case actions.LOGIN_SUCCESS:
        draft.loggedIn = true;
        draft.loggingIn = false;
        draft.user = action.user;
        break;
      case actions.LOGIN_FAILURE:
        draft.loggedIn = false;
        draft.loggingIn = false;
        draft.user = {};
        draft.error = action.error;
        break;
      case actions.LOGOUT:
        draft.loggedIn = false;
        draft.loggingIn = false;
        draft.user = {};
        break;
      default:
        break;
    }
  });
}
// Login Saga
export function* authorizeSaga(username, password) {
  try {
    const user = yield call(userService.login, username, password);
    yield put({ type: actions.LOGIN_SUCCESS, user });
  } catch (error) {
    yield put({ type: actions.LOGIN_FAILURE, error });
  } finally {
    if (yield cancelled()) {
      yield put({ type: actions.LOGIN_CANCELLED });
    }
  }
}
// Logout Saga
function* logout () {
  // dispatches the USERS_LOGOUT action
  yield put(actions.logoutAction())
  // remove our token
  yield call(userService.logout);
}
// Watcher Saga
export function* loginWatcher() {
  debugger;
  // Generators halt execution until their next step is ready/occurring
  // So it's not like this loop is firing in the background 1000/sec
  // Instead, it says, "okay, true === true", and hits the first step...
  while (true) {
    //
    // ... and in this first it sees a yield statement with `take` which
    // pauses the loop.  It will sit here and WAIT for this action.
    //
    // yield take(ACTION) just says, when our generator sees the ACTION
    // it will pull from that ACTION's payload that we send up, its
    // email and password.  ONLY when this happens will the loop move
    // forward...
    const { username, password } = yield take(actions.LOGIN_REQUEST);

    // ... and pass the email and password to our loginFlow() function.
    // The fork() method spins up another "process" that will deal with
    // handling the loginFlow's execution in the background!
    // Think, "fork another process".
    //
    // It also passes back to us, a reference to this forked task
    // which is stored in our const task here.  We can use this to manage
    // the task.
    //
    // However, fork() does not block our loop.  It's in the background
    // therefore as soon as our loop executes this it mores forward...
    const task = yield fork(authorizeSaga, username, password);

    // ... and begins looking for either CLIENT_UNSET or LOGIN_ERROR!
    // That's right, it gets to here and stops and begins watching
    // for these tasks only.  Why would it watch for login any more?
    // During the life cycle of this generator, the user will login once
    // and all we need to watch for is either logging out, or a login
    // error.  The moment it does grab either of these though it will
    // once again move forward...
    const action = yield take([actions.LOGIN_FAILURE, actions.LOGOUT]);

    // ... if, for whatever reason, we decide to logout during this
    // cancel the current action.  i.e. the user is being logged
    // in, they get impatient and start hammering the logout button.
    // this would result in the above statement seeing the CLIENT_UNSET
    // action, and down here, knowing that we should cancel the
    // forked `task` that was trying to log them in.  It will do so
    // and move forward...
    if (action.type === actions.LOGOUT) yield cancel(task);

    // ... finally we'll just log them out.  This will unset the client
    // access token ... -> follow this back up to the top of the while loop
    yield call(logout);
  }
}
// Module
export function getUsersModule(): ISagaModule<IUserState> {
  return {
    id: "auth",
    reducerMap: {
      auth: authReducer
    },
    sagas: [loginWatcher],
    // Actions to fire when this module is added/removed
    // initialActions: []
    // finalActions: [],
  };
}

In the Store.Js I needed to Run this Watcher:

// Store.js
const store = createStore(
  /* initial state */
  {},
  /** enhancers **/
  [composeWithDevTools(), routerMiddleware(history)],
  /* extensions to include */
  [getSagaExtension()],
  getUsersModule(),
);

// Need to Run the watcher here.

One option that I've tried:

// Store.js
var authModule = getUsersModule();
const sagaExtension = getSagaExtension();

const store = createStore(
  /* initial state */
  {},
  /** enhancers **/
  [composeWithDevTools(), routerMiddleware(history)],
  /* extensions to include */
  [sagaExtension],
  authModule
);

// Need to Run the watcher here.
sagaExtension.middleware[0].run(authModule.sagas[0]);

but with this I get the following Error:

Error: Before running a Saga, you must mount the Saga middleware on the Store using applyMiddleware

How do I run a Watcher? Or is there another better solution?

bloomdido commented 5 years ago

I just spent several hours trying to figure out the same problem. Looks like the interface to createStore was changed with this commit, but the documentation was never updated.

The example code was updated with this commit. That's what I used to figure out how to get it working...