0

The problem is that I have several inputs and one button to save the data. I have this method in my component in react:

handleClick(e) {
    e.preventDefault();

    this.props.newEmail ? this.props.onSaveNewEmail(this.props.newEmail) : null;
    this.props.newName ? this.props.onSaveNewName(this.props.newName) : null;
  }

And this method is captured in my redux saga:

export function* isName() {
  const name = yield select(makeNewNameSelector());
  ...

  // validation name
  if (!re.test(name)) {
    ...
  } else {
    // ! OK, I CAN UPDATE NAME BECAUSE NEW NAME IS OK
  }
}

export function* isEmail() {
  const email = yield select(makeNewEmailSelector());
  const requestURL = `/api/users/isEmail/${email}`;
  ...

  // validation email
  if (!re.test(email)) {
    ...
  } else {
    try {
      const response = yield call(request, requestURL);
      if (!response.isEmail) {
        // ! OK, I CAN UPDATE EMAIL BECAUSE IT DOESN'T HAVE IN MY DB MYSQL
      }
    } catch (err) {
      ...
    }
  }
}

// this method send request PUT and this can update data:
function* saveData() {
  const name = yield select(makeNewNameSelector());
  const email = yield select(makeNewEmailSelector());
  ...

  try {
    const response = yield call(request, requestURL, {
      method: 'PUT',
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json',
        Authorization: `Bearer ${jwt}`,
      },
      body: JSON.stringify({
        name,
        email,
      }),
    });

    if (response.success) {
      name ? yield put(enterNewNameSuccessAction()) : null;
      email ? yield put(enterNewEmailSuccessAction()) : null;
      yield put(saveDataSuccessAction());
    }
  } catch (err) {
    ...
  }
}

export default function* settingsPageSaga() {
  yield takeLatest(ENTER_NEW_NAME, isName);
  yield takeLatest(ENTER_NEW_EMAIL, isEmail);
}

And now how should I call my saveData() method only once?

If in place of the comments I put a yield call(saveData), it will work, but this will send a two request! I would like to send only one PUT request.

and if I want to change the e-mail, I have to wait until my code will check if such e-mail exists in the database. if not, then only change action.

ReactRouter4
  • 165
  • 2
  • 13
  • If you came up with a different solution please, let us know... if it helped you, please accept my answer. Both could be really useful for other users looking for the same (or a similar) answer – NoriSte Apr 12 '19 at 14:16

1 Answers1

0

You need to play with Redux-Saga fork and cancel

Step by step, a solution to your specific implementation could be the following:

  • import some more effects from redux-saga: import { cancel, fork} from "redux-saga/effects";
  • let the isEmail saga dispatch an action at its very beginning to let the other sagas intercepts it and a reducer to set an emailCheckRunning flag. This action could be yield put(EMAIL_CHECK_START)
  • let the SAVE_EMAIL update the emailCheckRunning flag to false with a reducer
  • instead of // ! OK, I CAN UPDATE NAME BECAUSE NEW NAME IS OK, dispatch an action like yield put(SAVE_NAME)
  • instead of // ! OK, I CAN UPDATE EMAIL BECAUSE IT DOESN'T HAVE IN MY DB MYSQL, dispatch an action like yield put(SAVE_EMAIL)
  • in settingsPageSaga spawn/fork a new saga (the difference between them isn't relevant for your sample code, read here if you want to know more about it)
yield fork(saveDataSaga);
  • your saveDataSaga saga will look like this one
function* saveDataSaga() {
  let task;
  while(true) {
    // both ENTER_NEW_NAME and ENTER_NEW_EMAIL will start a new `saveDataNetworkFlow` saga
    yield take([ENTER_NEW_NAME, ENTER_NEW_EMAIL]);

    // but, before starting a new one, it cancels the previously forked `saveDataNetworkFlow` (if any)
    if(task) {
      yield cancel(task);
    }

    task = yield fork(saveDataNetworkFlow);
  }
}
  • and the saveDataNetworkFlow saga is the following
function* saveDataNetworkFlow() {
  const action = yield take([SAVE_NAME, SAVE_EMAIL]);
  if(action.type === SAVE_NAME) {
    // wait some milliseconds to let the `isEmail` saga run
    yield wait(100);
  }

  // I assume that the `emailCheckRunning` flag is stored on top of your state, update it based on the shape of your own state
  const emailCheckRunning = yield select((globalState) => globalState.emailCheckRunning);

  if(emailCheckRunning) {
    yield take(SAVE_EMAIL);
  }

  yield call(saveData);
}



Let's analyze the different cases:

  • simple CHANGE_NAME action

    • the saveDataSaga saga cancels any previous saveDataNetworkFlow saga
    • the SAVE_NAME action unlocks the saveDataNetworkFlow saga
    • the saveDataNetworkFlow saga wait for 100 milliseconds
    • there isn't any pending email check so the saveDataNetworkFlow call the saveData saga
  • simple CHANGE_EMAIL action with valid AJAX check

    • the saveDataSaga saga cancels any previous saveDataNetworkFlow saga
    • the SAVE_EMAIL will unlock the saveDataNetworkFlow saga
    • there isn't a pending email check (remember the SAVE_EMAIL action gets a reducer set emailCheckRunning to false)
    • the saveDataNetworkFlow call the saveData saga
  • simple CHANGE_EMAIL action with invalid AJAX check

    • the saveDataSaga saga cancels any previous saveDataNetworkFlow saga
    • the SAVE_EMAIL will unlock the saveDataNetworkFlow saga
    • there is a pending email check so the saveDataNetworkFlow waits for the SAVE_EMAIL action
    • the SAVE_EMAIL won't be dispatched, the saveDataNetworkFlow saga never ends and will be eventually canceled in the future
  • CHANGE_NAME and CHANGE_EMAIL with valid AJAX check

    • the saveDataSaga saga cancels any previous saveDataNetworkFlow saga
    • the SAVE_NAME action unlocks the saveDataNetworkFlow saga
    • the saveDataNetworkFlow saga wait for 100 milliseconds
    • there is a pending email check so the saveDataNetworkFlow waits for the SAVE_EMAIL action
    • the isEmail saga dispatches a SAVE_EMAIL
    • the saveDataNetworkFlow call the saveData saga
  • CHANGE_NAME and CHANGE_EMAIL with invalid AJAX check

    • the saveDataSaga saga cancels any previous saveDataNetworkFlow saga
    • the SAVE_NAME action unlocks the saveDataNetworkFlow saga
    • the saveDataNetworkFlow saga wait for 100 milliseconds
    • there is a pending email check, so the saveDataNetworkFlow waits for the SAVE_EMAIL action
    • the SAVE_EMAIL won't be dispatched, the saveDataNetworkFlow saga never ends and will be eventually canceled in the future
  • CHANGE_NAME or CHANGE_EMAIL action dispatched while a previously saga is running

    • you need to implement the same cancellation logic for the isName and isEmail sagas to avoid any concurrency problem, here you can find how to do it (it's the same guide that's at the base of this solution of mine)

I hope it could be helpful, let me know if you need something more


Note that:

  • if the user submits the form twice while the first saveData is still waiting for the server response... two AJAX requests are going to hit the server. It's up to you to manage the Redux state with some reducers and avoiding the user to submit the form while the first request isn't finished yet. It doesn't make sense to cancel the AJAX request because once it has left the client, it could already take effect on the server.
  • every canceled saga can manage its cancellation policy, the code to manage it is the following
import { cancelled } from 'redux-saga/effects'
function* saga() {
  try {
    // ... your code
  } finally {
    if (yield cancelled())
      // the saga has been cancelled
  }
}
NoriSte
  • 3,589
  • 1
  • 19
  • 18