1

I am new to functional programming.

As I heard many times, functional programming could help with easier maintenance. So, I would like to see if it can help with this problem, or actually I may need to look for other solution. If other solution is needed, sorry for this misleading topic.

In my react project,

I have two states, one is Projection, other is called StressProjections

I have a function that will invoke two Api call, once the api return come back. It will update both state

Below is my code.

//call API1
const simulateionCall = new Promise((resolve, reject) => {
  axios.post <
    IProjectionResponse >
    (`/api1`, normalProjectionRequest)
      .then((data) => {
        resolve(data);
      })
      .catch((err) => {
        reject(err);
      });
});

//call API12
const stressCall = new Promise((resolve, reject) => {
  axios.post <
    IStressProjectionResponse >
    (`/api2`, stressProjectionRequest)
      .then((data) => {
        resolve(data);
      })
      .catch((err) => {
        reject(err);
      });
});

Promise.allSettled([simulateionCall, stressCall]).then((vals) => {
  console.log(vals);
  vals.forEach((val, index) => {
    if (val.status === "rejected") {
      if (index === 0) {
        //ERROR handling api1
        props.setProjection(resMock.results);
      } else {
        //ERROR handling api22
        props.setStressProjections(res.results[0].scenarios);
      }
    } else {
      if (index === 0) {
        //Set content for api 1
        // @ts-ignore
        props.setProjection(val.value.data.result);
      } else {
        //Set content for api 2
        // @ts-ignore
        props.setStressProjections(val.value.data.results);
      }
    }
  });
});

Question:

In my allsettle function, as you can see I need to check with the index before manually to assign which props.set function and which error handling should be call.

First 1, it is not readable, as some may wonder why we use this set function when it is in index 0.

Second2, it is hard in maintenance. When there are more and more Api call is included, my code would be full of conditional if (index === ...) , it increase the diffculty in reading

Hope I can see a new technique or functional way to cope with above two issues.

Ae Leung
  • 300
  • 1
  • 12

2 Answers2

2

avoid the explicit promise construction antipattern

const simulateionCall = new Promise((resolve, reject) => {
  axios.post <
    IProjectionResponse >
    (`/api1`, normalProjectionRequest)
      .then((data) => {
        resolve(data);
      })
      .catch((err) => {
        reject(err);
      });
});

is the same as -

const simulateionCall = 
  axios.post<IProjectionResponse>("/api1", normalProjectionRequest)

you make the same mistake with stressCall.

don't use Promise.allSettled

You are seeing the obvious disadvantages of using Promise.allSettled, but there's no reason to use it in the first place. Your promises can run in separate "threads" to avoid tangling them together and having to keep track of their indexes.

axios
  .post<IProjectionResponse>("/api1", normalProjectionRequest)
  .then(data => props.setProjection(data.result))
  .catch(console.error) // or something else

axios
  .post<IStressProjectionResponse>("/api2", stressProjectionRequest)
  .then(data => props.setStressProjections(data.results))
  .catch(console.error) // or something else

react

You tagged this with react. If you want these two posts to happen when the user presses a button -

const onClick = (event) => {
  axios
    .post<IProjectionResponse>("/api1", normalProjectionRequest)
    .then(data => props.setProjection(data.result))
    .catch(console.error) // or something else
  axios
    .post<IStressProjectionResponse>("/api2", stressProjectionRequest)
    .then(data => props.setStressProjections(data.results))
    .catch(console.error) // or something else
}

return <>
  ...
  <button type="button" onClick={onClick}>Submit</button>
</>

If you want to re-run these posts requests each time the normal and stress projection requests change, use useEffect with normalProjectionRequest and stressProjectionRequest as dependencies of the effect -

useEffect(() => {
  axios
    .post<IProjectionResponse>("/api1", normalProjectionRequest)
    .then(data => props.setProjection(data.result))
    .catch(console.error)
  axios
    .post<IStressProjectionResponse>("/api2", stressProjectionRequest)
    .then(data => props.setStressProjections(data.results))
    .catch(console.error)
}, [normalProjectionRequest, stressProjectionRequest])
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • Yea,Thanks, but the reason why I want to use explicit call is that I want to use resolve and reject function here. So, I just think probably this is where I cant optimized, Let me know if I misunderstood you. – Ae Leung Aug 17 '22 at 03:34
  • Ae, the point I’m making is that there’s no need for the explicit resolve/reject functions in this code and they only serve to confuse you and make your code 5x longer. It’s called an anti-pattern because it actually goes against the way promises were designed to be used. The examples shared here do the exact same thing without the messy anti-pattern. – Mulan Aug 17 '22 at 05:42
  • Sorry, maybe I misunderstood you. But only having "const simulateionCall = axios.post("/api1", normalProjectionRequest) " doesnt seems to does the same as I did? Coz this is exactly a part of me code, but without the resolve and reject. But without both, how could this be the same results as I did? – Ae Leung Aug 17 '22 at 14:51
  • 1
    `axios.post` _already_ returns a promise, so wrapping it in another `new Promise((resolve, reject) => ...)` does exactly nothing. I made some updates to this answer to provide additional context. Please let me know if you have a follow up question – Mulan Aug 17 '22 at 15:03
  • I guess I understand what you mean, but even without wrapping it with Promise can also return me a Promise, it can not do resolve/ reject() handling with that promise. Actually the only reason why I wraping it by a Promise is that I want to use resolve()/reject() ..... Let me know if I was wrong, Thanks for providing extra information on the allsettle(). Actually, except the concern of having to track the index in allsettle(), using allsettle() is my intention. As you can see, after each api request, I want to use the setup function to update the state. If I use the way you did... – Ae Leung Aug 17 '22 at 16:06
  • the way you did, the problem is that it may render before the data get setted. I come across this before. That's why I explicitly use a allsettle at the end to make what need to be handled get done before renderering. – Ae Leung Aug 17 '22 at 16:09
  • Let me know if you dont understand what I mean, as it is a bit long – Ae Leung Aug 17 '22 at 16:10
  • 1) "I want to use resolve/reject", for what purpose, other than just to use them? the explicit promise contructor antipattern uses resolve/reject but provides no difference in behaviour or meaning to the program - it only adds more syntax boilerplate and confusion to the writer. 2) it will not render before the data gets settled, but you are correct there will be two renders in total. in general, this is not a problem as different subcomponents can update as their data arrives. it's common to show "Loading..." or a spinner while the data is in transit... – Mulan Aug 17 '22 at 16:19
  • 1
    (continued) if the goal is to wait for _all_ data before doing any render, Promise.allSettled is still a bad fit. When writing functional programs, you have to decide what is going to happen for all states of the program. So you have some questions to answer on that: If _either_ request fails, can any partial result be rendered? If so, use the "threaded" approach in my answer. Otherwise if a partial result cannot be rendered and an error is to be displayed, use Promise.all as it captures your intention more accurately... – Mulan Aug 17 '22 at 16:22
  • 1
    (continued) The fact that you have two setters, `props.setProject` and `props.setStressProjections` to me indicates that it's fine for your components to update these "slices" of state independently, and that you gain nothing by combining the two requests and trying to pick the results apart afterwards. If two requests were expected to update the state once using a _single_ setter, I could see the use case for Promise.all. – Mulan Aug 17 '22 at 16:24
0

Adding handler as part of the resolve helps avoid the if conditions.

const mockAxiosPost = (url, body) => {
  return new Promise((resolve) => {
    setTimeout(() => resolve(body), body.key);
  });
};

const normalProjectionRequest = {
  key: 1000,
};
const stressProjectionRequest = {
  key: 3000,
};

//call API1
const simulateionCallHandler = console.log;

const simulateionCall = new Promise((resolve, reject) => {
  mockAxiosPost(`/api1`, normalProjectionRequest)
    .then((data) => {
      resolve({
        data: data,
        handler: simulateionCallHandler,
      });
    })
    .catch((err) => {
      reject(err);
    });
});

//call API12
const stressCallHandler = console.log;

const stressCall = new Promise((resolve, reject) => {
  mockAxiosPost(`/api2`, stressProjectionRequest)
    .then((data) => {
      resolve({
        data: data,
        handler: stressCallHandler,
      });
    })
    .catch((err) => {
      reject(err);
    });
});

const props = {
  setProjection: console.log,
  setStressProjections: console.log,
};

Promise.allSettled([simulateionCall, stressCall]).then((vals) => {
  vals.forEach((val, index) => {
    console.log(vals);
    if (val.status === 'rejected') {
      if (index === 0) {
        //ERROR handling api1
        props.setProjection(resMock.results);
      } else {
        //ERROR handling api22
        props.setStressProjections(res.results[0].scenarios);
      }
    } else {
      val.value.handler(val.value.data);
    }
  });
});
user_ame
  • 9
  • 1