1

I'm struggling a bit with using custom react hooks.

I got 2 custom hooks.

First hook is for fetching a ID, the second one is used to fetch a profile with this previous fetched ID. It is dependent on that ID so I need to await this promise.

I have the following custom hook:

export const UseMetamask = () => {

    //Todo: Create check if metamask is in browser, otherwise throw error
    const fetchWallet = async (): Promise<string | null> => {
        try {
            const accounts: string[] = await window.ethereum.request(
                {
                    method: 'eth_requestAccounts'
                },
            );

            return accounts[0];

        } catch(e) {
            console.error(e);

            return null;
        }
    }
    
    return fetchWallet();
}

Then in my second hook I have:

const wallet = UseMetamask();

which is then used in a react-query call like:

useQuery(
    ['user', wallet],
    () => getUserByWallet(wallet),

Now it complains on the wallet about it being a Promise<string | null> which is ofcourse not suitable for the getUserByWallet.

What is the go to way to wait for another hook then use that result in a second hook?

Thanks!

Ori Drori
  • 183,571
  • 29
  • 224
  • 209
Wesley van S
  • 69
  • 1
  • 12
  • Your first hook isn't using any react state or methods. It's better served as a normal function. You can use await on the second parameter of `useQuery` – Badal Saibo Feb 03 '23 at 09:11
  • @BadalSaibo Interesting! This might just be the trick. Making the queryFn async will work I think. – Wesley van S Feb 03 '23 at 09:19

3 Answers3

2

A functional component is a synchronous function, and as a component has life cycle hooks. The asynchronous calls are side effects that should be handled by hooks, not by passing promises in the body of the function. See this SO answer.

Option 1 - using useEffect with useState:

Wrap the api call in useEffect and set the wallet state when the api call succeeds. Return the wallet state from the hook:

export const useMetamask = () => {
  const [wallet, setWallet] = useState<string | null>(null);
  
  useEffect(() => {
    const fetchWallet = async(): Promise<string | null> => {
      try {
        const accounts: string[] = await window.ethereum.request({
          method: 'eth_requestAccounts'
        });

        setWallet(accounts[0]);

      } catch (e) {
        console.error(e);

        return null;
      }
    }
    
    fetchWallet();
  }, []);

  return wallet;
}

Usage:

Get the wallet from the hook. This would be null or the actual value:

const wallet = useMetamask();

Only enable the call when a wallet actually exists (not null). We'll use the enable option (see Dependent Queries), to enable/disable the query according to the value of wallet:

useQuery(
  ['user', wallet],
  () => getUserByWallet(wallet),
  {
  // The query will not execute until the wallet exists
  enabled: !!wallet,
  }
)

Option 2 - use two useQuery hooks

Since you already use useQuery, you need to manually write a hook. Just get the wallet from another useQuery call:

const wallet useQuery('wallet', fetchWallet);

useQuery(
  ['user', wallet],
  () => getUserByWallet(wallet),
  {
  // The query will not execute until the wallet exists
  enabled: !!wallet,
  }
)
Ori Drori
  • 183,571
  • 29
  • 224
  • 209
  • Great explanation thank you, do you think the UseMetamask hook could be a simple function instead that is resolved in the () => getUserByWallet? This way tho the enabled is not dependant on the wallet. Torn on what the better solution is here as im pretty new to react and writing hooks. – Wesley van S Feb 03 '23 at 09:37
  • If need the `wallet` value for `useQuery` (or for any other use), you can't make the call inside it. See the 2nd option of my answer. – Ori Drori Feb 03 '23 at 09:50
  • Will be using the hook one still because it makes more sense I think (the fetching of wallet isn't really a api call). Thanks! – Wesley van S Feb 03 '23 at 10:02
  • 1
    @WesleyvanS As a rule of hooks, rename `UseMetaMask` to `useMetaMask` [see here](https://reactjs.org/docs/hooks-custom.html#extracting-a-custom-hook) – Badal Saibo Feb 03 '23 at 10:17
  • Yes did that, cheers! – Wesley van S Feb 03 '23 at 10:28
0

It is a bad idea to create a hook then just return a single function out of it. And it is a promise too on top of that. Return an object from your hook instead. Then await it in your caller.

export const useMetamask = () => {

    //Todo: Create check if metamask is in browser, otherwise throw error
    const fetchWallet = async (): Promise<string | null> => {
        try {
            const accounts: string[] = await window.ethereum.request(
                {
                    method: 'eth_requestAccounts'
                },
            );

            return accounts[0];

        } catch(e) {
            console.error(e);

            return null;
        }
    }
    
    return { fetchWallet };
}

Then in your caller

const { fetchWallet } = useMetamask();
const wallet = await fetchWallet();

useQuery(
    ['user', wallet],
    () => getUserByWallet(wallet),

Also, please use a small letter 'useSomething' in your hooks to differentiate it from your UI components

JkAlombro
  • 1,696
  • 1
  • 16
  • 30
  • Fair enough, was refactoring this as both of these were in 1 hook before. Still tho the problem here is the same. await fetchWallet(); won't work as the caller is also a custom hook. This means I would have to make the main hook async. Don't think this is good practice right? – Wesley van S Feb 03 '23 at 09:16
  • Not sure why you're so obsessed in making these a hook though. By the looks of the function you're trying to return, you can just create it as an ordinary function and just import it to your caller. You can even make it a something like an API service to make things more organized. – JkAlombro Feb 03 '23 at 09:20
  • I'm not! I just had this massive 1 hook first and definetly see how it probably shouldn't be a hook. Will change that. – Wesley van S Feb 03 '23 at 09:22
0

You need to use useState in the custom hook.

// move fetchWallet function to utils and import it here for better code smells

export const useMetamask = () => {
    const [wallet, setWallet] = useState(null);
    // you do not want to fetch wallet everytime the component updates, You want to do it only once.
    useEffect(()=>{
        fetchWallet().then(wallet => setWallet(wallet)).catch(errorHandler);
    }, [])
    return wallet;
}

In other hooks, check if wallet is null and handle accordingly.

Mayank Kumar Chaudhari
  • 16,027
  • 10
  • 55
  • 122