2

I created an usePromise React Hook that should be able to resolve every kind of javascript promise and returns every result and state: data, resolving state and error. Im able to make it work passing the function without any param, but when I try to change it to allow a param, I get an infinite loop.

const usePromise = (promise: any): [any, boolean, any] => {
  const [data, setData] = useState<object | null>(null);
  const [error, setError] = useState<object | null>(null);
  const [fetching, setFetchingState] = useState<boolean>(true);

  useEffect(() => {
    setFetchingState(true);
    promise
      .then((data: object) => {
        setData(data);
      })
      .catch((error: object) => {
        setError(error);
      })
      .finally(() => {
        setFetchingState(false);
      });
  }, [promise]);

  return [data, fetching, error];
};

const apiCall = (param?: string): Promise<any> => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({ response: `Response generated with your param ${param}.` });
    }, 500);
  });
};

const App = (): React.Element => {
  // How can I pass an argument to apiCall?
  const [response, fetching, error] = usePromise(apiCall(5));
  console.log("render"); // This logs infinitely
  return <div>{JSON.stringify({ response, fetching, error })}</div>;
};

You can check the working code (without params) at: https://codesandbox.io/s/react-typescript-fl13w

And the bug at (The tab gets stuck, be adviced): https://codesandbox.io/s/react-typescript-9ow82

Note: I would like to find the solution without using a usePromise single function library from NPM or similar

Rashomon
  • 5,962
  • 4
  • 29
  • 67
  • does your code handles , promises.all type of response ? if not then remove the [promise] as dependency and make call like () => apiCall(5) inside usePromise – 0xAnon Oct 28 '19 at 19:01

1 Answers1

2

Custom hooks might be executed multiple times. You should design it that way, that everything you want to do just once (e.g. the API call) is inside a useEffect hook. That can be achieved by taking a callback that gets then called in a hook.

Also, slightly more typesafe:

 const usePromise = <T>(task: () => Promise<T>) => {
   const [state, setState] = useState<[T?, boolean, Error?]>([null, true, null]);


   useEffect(() => {
       task()
         .then(result => setState([result, false, null])
         .catch(error => setState([null, false, error]);      
  }, []); // << omit the condition here, functions don't equal each other²

  return state;
};

// Then used as 
usePromise(() => apiCall(5));

² yes, thats generally a bad practice, but as task is not supposed to change here, I think that's fine


Upon request, here's a version that I use in some of my projects:

 export function useAPI<Q, R>(api: (query: Q) => Promise<R | never>) {
  const [state, setState] = useState<{ loading?: true, pending?: true, error?: string, errorCode?: number, result?: R }>({ pending: true });

  async function run(query: Q) {
    if(state.loading) return;
    setState({ loading: true });

    try {
        const result = await api(query);
        setState({ result });
    } catch(error) {
        if(error instanceof HTTPError) {
            console.error(`API Error: ${error.path}`, error);
            setState({ error: error.message, errorCode: error.code });
        } else {
            setState({ error: error.message, errorCode: NaN });
        }
    }
  }

  function reset() {
     setState({ pending: true });
  }

  return [state, run, reset] as const;

}
Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
  • the reason being ```useEffect``` has dependency ```promise``` which is a function which contains ```setTimeOut```/ ```promise``` and everytime the component renders , a new setTimeout is being pushed to the callbackQueue from the apis section , thereby infinite async and causing re-rendering – 0xAnon Oct 28 '19 at 19:10
  • Hey @Jonas thank you very much for your answer. I found out that, if the component is mounted and the param of `apiCall` changes, the `usePromise` result is not updated. I think that it should receive the params of the function as the hook comparator (second param of the `useEffect`). What do you think? https://codesandbox.io/s/react-typescript-bgqrp – Rashomon Oct 28 '19 at 19:25
  • 1
    That's difficult. What should happen to the currently ongoing API call? Make sure to get cancellation right, and also be careful with the parameters. Objects are compared by reference in react. – Jonas Wilms Oct 28 '19 at 19:27
  • Yeah, thats a good point! If you have any `usePromise` or generic solution better than mine, please share. Thank you very much! – Rashomon Oct 28 '19 at 19:30
  • @rashomon Yeah, I do have one that I use often, not sure if it fits to your usecase exactly. – Jonas Wilms Oct 28 '19 at 19:37