Ad

How To Handle Common Fetch Actions Inside Saga

- 1 answer

I'm developping an API consuming web front site.

The problem

All my API saga were like this :

export function* login(action) {
  const requestURL = "./api/auth/login"; // Endpoint URL
  //  Select the token if needed : const token = yield select(makeSelectToken());

  const options = {
    method: 'POST',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + btoa(JSON.stringify({ login: action.email, password: action.password })),
    }
  };

  try {
    // The request helper from react-boilerplate
    const user = yield call(request, requestURL, options);
    yield put(loginActions.loginSuccess(user.token);
    yield put(push('/'));
  } catch (err) {
    yield put(loginActions.loginFailure(err.detailedMessage));
    yield put(executeErrorHandler(err.code, err.detailedMessage, err.key)); // Error handling
  }
}

And I had the same pattern with all my sagas :

  • Select the token if I need to call a private function in the start of the saga

const token = yield select(makeSelectToken());

  • Handle errors on the catch part
export const executeErrorHandler = (code, detailedMessage, key) => ({
  type: HTTP_ERROR_HANDLER, status: code, detailedMessage, key
});

export function* errorHandler(action) {
  switch (action.status) {
    case 400:
      yield put(addError(action.key, action.detailedMessage));
      break;

    case 401:
      put(push('/login'));
      break;

    //other errors...
  }
}

export default function* httpError() {
  yield takeLatest(HTTP_ERROR_HANDLER, errorHandler);
}

The solution I came up with

Remove the token parts and error handling part and puth them inside the call helper :

export function* login(action) {
  const url = `${apiUrl.public}/signin`;

  const body = JSON.stringify({
    email: action.email,
    password: action.password,
  });

  try {
    const user = yield call(postRequest, { url, body });

    yield put(loginSuccess(user.token, action.email));
    yield put(push('/'));
  } catch (err) {
    yield put(loginFailure());
  }
}
// post request just call the default request with a "post" method
export function postRequest({ url, headers, body, auth = null }) {
  return request(url, 'post', headers, body, auth);
}

export default function request(url, method, headers, body, auth = null) {
  const options = { method, headers, body };

  return fetch(url, addHeader(options, auth)) // add header will add the token if auth == true
    .then(checkStatus)
    .then(parseJSON)
    .catch(handleError); // the error handler
}

function handleError(error) {
  if (error.code === 401) {
    put(push('/login')); // <-- Here this doesn't work
  }

  if (error.code == 400) {
    displayToast(error);
  }
}

function addHeader(options = {}, auth) {
  const newOptions = { ...options };
  if (!options.headers) {
    newOptions.headers = {
      Accept: 'application/json',
      'Content-Type': 'application/json',
      ...options.headers,
    };
  }

  if (auth) {
    const token =  yield select(makeSelectToken()); // <-- here it doesn't work
    newOptions.headers.Authorization = `Bearer ${auth}`;
  }

  return newOptions;
}

I know the solution is between generator functions, side effects, yield call / select but I tried so many things it didn't work. For example, if I wrap everything inside generator functions, the token load is executed after the code continues and call the API.

Your help would be appreciated.

Ad

Answer

You need to run any and all effects (e.g. yield select) from a generator function, so you'll need generators all the way down to the point in your call stack where you yield an effect. Given that I would try to push those calls as high as possible. I assume you may have getRequest, putRequest etc. in addition to postRequest so if you want to avoid duplicating the yield select you'll want to do it in request. I can't fully test your snippet but I believe this should work:

export function* postRequest({ url, headers, body, auth = null }) {
  return yield call(request, url, 'post', headers, body, auth); // could yield directly but using `call` makes testing eaiser
}

export default function* request(url, method, headers, body, auth = null) {
  const options = { method, headers, body };
  const token = auth ? yield select(makeSelectToken()) : null;
  try {
      const response = yield call(fetch, url, addHeader(options, token));
      const checkedResponse = checkStatus(response);
      return parseJSON(checkedResponse);
  } catch (e) {
     const errorEffect = getErrorEffect(e); // replaces handleError
     if (errorEffect) {
        yield errorEffect;
     }
  }
}

function addHeader(options = {}, token) {
  const newOptions = { ...options };
  if (!options.headers) {
    newOptions.headers = {
      Accept: 'application/json',
      'Content-Type': 'application/json',
      ...options.headers,
    };
  }

  if (token) {
    newOptions.headers.Authorization = `Bearer ${token}`;
  }

  return newOptions;
}

function getErrorEffect(error) {
  if (error.code === 401) {
    return put(push('/login')); // returns the effect for the `request` generator to yeild
  }

  if (error.code == 400) {
    return displayToast(error); // assuming `displayToast` is an effect that can be yielded directly
  }
}
Ad
source: stackoverflow.com
Ad