pikax/vue-composable

How to chain thens in usePromise?

wizardnet972 opened this issue · 5 comments

How to chain .thens functions and adjust the loading property to be true after all the thens complete?

I have a function getItems that return array with ids after 20 seconds (like http).

export function timeout(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

const getItems = () => timeout(20 * 1000).then(() => [{ id: 1 }, { id: 2 }]);

and I call this function via usePromise:

const useApi = () => {
const api = usePromise(() => getItems(), { lazy: true });
return api;
};

In the setup function I call to useApi and pass the loading to the template, and I get the r with the data.

const App = {
  template: `
  <div>
    <h1>im app {{loading}}</h1>
  </div>
  `,
  setup() {
    const { exec, loading } = useApi();

    exec().then(r => {
      console.log({ r });
    });

    return { loading };
  }
};

But what if I want to add other function without change the useApi and I want loading to not change until all of the then complete.

exec().then(async r => {
  console.log({ r });  // <--- the `loading` is true :(
  await timeout(20 * 1000);  
 // <-- here, now the `loading` should be true.
});

I want to wait another 20 seconds. but after the first then complete it set loading to ture.

@pikax There is something I can do to make it works?
my example on codesandbox.io

pikax commented

When you do .then it will create a new promise, making the useApi not be aware of it.

What you can do is passing the function to the exec:

const useApi = () => {
  const api = usePromise(p => getItems().then(p), { lazy: true });

  return api;
};


//setup
    exec(async r => {
      console.log({ r, loading });

      await timeout(20 * 1000);
    });

codesandbox

@pikax thanks for your answer.

But what I think of is to use useApi as it is. and sometimes I want to do something after, and I want to control it in the host function.

I think about something like this to catch the error or the promise. instead of try..catch..finally .

export const scope = (fn) => (req, res, next, ...args) => Promise.resolve(fn(req, res, next, ...args)).catch(next); // this is how I handle promise with nodejs

This is mean something like Promise/finally:

 const promise = new Promise((res) => { setTimeout(() => res(), 10* 1000) });
  ...
  Promise.resolve(promise)
   .catch(e => )
   .finally(() => loading = false)
  ...
 promise.then(r => ...);

so here you will get the catch when error, and somebody can hook into the original promise. (and the finally always happens)

pikax commented

I don't think you can hook to the original promise.

export function timeout(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

const getItems = () => timeout(1000).then(() => [{ id: 1 }, { id: 2 }]);

function myPromise(p) {
  const loading = ref(true);
  const promise = Promise.resolve(p).finally(() => (loading.value = false));
  return {
    promise,
    loading
  };
}

export default defineComponent({
  name: "App",

  setup() {
    const promise = myPromise(getItems());

    promise.promise.then(async x => {
      console.log("resolved", x);
      await timeout(2000);
      console.log("rrr ", promise.loading.value); // this  is always false
    });

    return promise;
  }
});

sandbox

That's a javascript behaviour, if you can provide a working example of hooking into a promised already created I really appreciate it :)

@pikax Here what I suggest. I rewrite the usePromise (with same signature) but a different way to hook the promise.

What do you think about it? can I send PR?

sandbox

<template>
  <div id="app">Loading: {{loading}}</div>
</template>

<script>
import { defineComponent, toRefs, reactive } from "@vue/composition-api";

export function timeout(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

const getItems = () =>
  timeout(1000)
    .then(() => {
      console.log("in getItems..");
    })
    .then(() => [{ id: 1 }, { id: 2 }]);

const usePromise = (fnOrPromise, options = { lazy: false }) => {
  const state = reactive({
    loading: false,
    error: null,
    promise: typeof fnOrPromise === "function" ? fnOrPromise() : fnOrPromise,
    exec: () => {
      state.loading = true;
      state.error = null;

      const executePromise = Promise.resolve(state.promise)
        .catch(e => (state.error = e))
        .finally(() => {
          console.log("in finally");
          state.loading = false;
        });

      return executePromise;
    }
  });

  if (!options.lazy) {
    state.exec();
  }

  return toRefs(state);
};

export default defineComponent({
  name: "App",

  setup() {
    console.clear();
    console.log("in setup");

    const { exec, loading, promise } = usePromise(() => getItems(), {
      lazy: true
    });

    promise.value.then(r => {
      console.log({ r });
      console.log({ l: loading.value });
    });

    exec.value().then(t => {
      console.log({ t });
      console.log({ l: loading.value });
    });

    return { loading };
  }
});
</script>


pikax commented

That usePromise actually acts a bit different from what usePromise currently behaves.

The value is only resolved once, no matter how many times you call exec.value it will always resolve the first promise (won't call the factory anymore)