Custom hook returning a promis - reactjs

Is it possible to return a promise from a custom react hook and await for it? Like:
const getData = useMyCustomGetDataHook();
const fetchData = async () => {
await getData({...props});
// ... do stuff..
}

In reality, there is no such thing as a custom React Hook - it is simply a fancy name for a function having a name that starts with use. Thus, any function can be a custom React Hook - including a function that returns a Promise that you can await.
However the real problem is that your async function won't be usable inside a component. render() is not async and you cannot await. In fact a custom React Hook is more about the programming paradigm that the function itself.
What you need to do in your case is to launch your async operation either from useEffect() - if it does not require that DOM is created, or from useLayoutEffect() - if it requires it - and then trigger a state change in the .then() handler. This is the React way of doing this.

You can't directly await for a promise while rendering (aside from the experimental suspense stuff).
You could use e.g. useSWR() as a wrapper for a promise-returning function (which, wink wink, could be e.g. something that uses fetch()...).
async function getData(x) {
return x * 4;
}
function useData(x) {
return useSWR(['myData', x], (_, x) => getData(x));
}
function MyComponent() {
const dataSWR = useData(8);
if(dataSWR.data === undefined) {
return <>Loading...</>;
}
return <>{dataSWR.data}</>;
}

Related

Why "React hook is called in a function" while exporting simple single function that accesses a useAtom state?

I have this simple function that needs to set a state from jotai. I would like this function to have it's own seperate file or be cleaned up somewhere, since it will be reused. I'm new to React and come from Angular. It's kind of a function in a service Angular wise.
How would you solve this properly in React?
Code:
export const setMetamaskWallet = (): void => {
const [, setWeb3] = useAtom(web3Atom);
const [, setLoading] = useAtom(loadingAtom);
const [wallet, setWallet] = useAtom(walletAtom);
setWeb3(
new Web3(window.ethereum),
)
//Todo: Create check if metamask is in browser, otherwise throw error
const setAccount = async () => {
setLoading(true);
const accounts = await window.ethereum.request(
{
method: 'eth_requestAccounts'
},
);
setWallet(
accounts[0],
);
setLoading(false);
}
if (!wallet) {
setAccount();
}
}
Hooks can only be called in React functional components or other hooks. Here it appears you have called it in a typical function, hence the error. You could package this functionality in a custom hook. It may however be most appropriate to keep this function as it is, and instead of calling hooks within it, pass the relevant data into the function as parameters.

I receive an invalid hook call error in the search function

I was creating a function for my search bar but I receive this error:
Below is the function:
const HandleSearch = async (val) => {
useEffect(() => {
const fetchData = async () => {
const data = await db.collection('accounts').orderBy('date').get();
setAccounts(data.docs.map((doc) => ({ ...doc.data(), id: doc.id })));
};
fetchData();
}, []);
useEffect(() => {
setAccounts(
accounts.filter(
(account) =>
account.name.toLowerCase().includes(search.toLowerCase())
)
);
}, [search, accounts]);
}
you might be using the hooks in a wrong way. as per rules of hooks, hooks can only be called from React function components and from a custom Hooks. Don’t call Hooks from regular JavaScript functions, inside loops, conditions, or nested functions.
As the error message tells hooks can only be called inside function components.
Try removing the async keyword preceding your component. This way React will understand it is a functional component and everything should work fine :)
At the moment it is not actually a component as components are supposed to return JSX. By preceding it with the async keyword it will implicitly return a Promise.
You can read more about async functions here. Promises are a pretty tricky concept and take time to fully understand.

How to test internal functions using react hooks testing library?

I have a custom hook that I am trying to write tests for using the react hooks testing library package and I would like to know how I can test internal functions that are not returned in the custom hook but are used within other functions.
const customHook = () => {
const [count, setCount] = React.useState(0);
const doSomeThing = () => {
..code
}
const increment = () => {
doSomeThing(); //Would like to make assertations on this
setCount((x) => x + 1 );
}
return { count, increment }
}
export default customHook;
test
it('Should call increment', () => {
const { result } = renderHook(() => useCustomHook())
act(() => {
result.current.increment();
});
expect(doSomeThing).toHaveBeenCalled(); //end result of what I would like help on
});
How can I write a test to see if doSomething has been called/used?
You can't. It's entirely internal to that hook, and no handle is provided to get at that function. So you can't mock it, and you can't call it directly. It's impossible to test only the doSomething function as you have it.
And more to the point, you shouldn't. You don't want to test at that level. This is a private implementation detail. You should test the public interface of your hook. That means test the arguments, the return values, and how calling functions the hook returns affect the next returned values.
Tests shouldn't care how a function does its job. Tests should only care that the function does its job correctly.
That means your test can only verify what doSomething does, and not whether it's been called. And as of right now, it doesn't do anything at all, so there's nothing to test.

How to partially mock a custom react hook with Jest?

I would like to mock some of my custom React hook return value with Jest.
Here is an example of what i'm trying to do :
export default function useSongPlayer() {
const playSongA = () => {...}
const playSongB = () => {...}
const playSongC = () => {...}
return {
playSongA,
playSongB,
playSongC,
}
}
Now in my test, I would like to mock only playSongA and keep the other functions real implementations. I tried several approachs, including someting with jest.requireActual('hooks/useSongPlayer') but it seems that playSongA is successfully mocked while the others are just undefined.
With something like this, i'm pretty sure to achieve what i want
export function playSongA() {...}
export function playSongB() {...}
export function playSongC() {...}
The problem seems to be the module I try to mock is a function, and I want to partially mock the result of this function.
This is possible if you create a mock that returns the original, but also intercepts the function calls and replaces one of the values.
You can do this without a Proxy, but I choose to demonstrate that as it's the most complete method to intercept any use of the actual hook.
hooks/__mocks__/useSongPlayer.js
// Get the actual hook
const useSongPlayer = jest.requireActual('hooks/useSongPlayer');
// Proxy the original hook so everything passes through except our mocked function call
const proxiedUseSongPlayer = new Proxy(useSongPlayer, {
apply(target, thisArg, argumentsList) {
const realResult = Reflect.apply(target, thisArg, argumentsList);
return {
// Return the result of the real function
...realResult,
// Override a single function with a mocked version
playSongA: jest.fn(),
};
}
});
export default proxiedUseSongPlayer;

React Hooks , function fetchData is not a react component?

I'm playing around with react hooks and I ran into a weird issue trying to fetch some data, when i'm creating a file to fetch data using hooks and axios
this works
import axios from 'axios';
const useResources = (resource) => {
const [resources, setResources ] = useState([]);
useEffect(
() => {
(async resource => {
const response = await axios.get(`randomapi.com/${resource}`);
setResources(response.data);
})(resource);
},
[resource]
);
return resources;
};
export default useResources;
but this doesn't
import { useState, useEffect } from 'react';
import Axios from 'axios';
const fetchData = () => {
const [data, setData] = useState('');
useEffect( async () => {
const response = await Axios('randomapi.com/word?key=*****&number={number_of_words}');
setData(response.data);
});
return data;
};
export default fetchData;
'React Hook useEffect contains a call to 'setData'. Without a list of dependencies, this can lead to an infinite chain of updates. To fix this, pass [] as a second argument to the useEffect Hook.'
Aren't they the same?
On first glance, they are similar, but they still have differences.
Let's check them:
useEffect(
() => {
// we create a function, that works like a "black box" for useEffect
(async resource => {
const response = await axios.get(`randomapi.com/${resource}`);
// we don't use `setResources` in dependencies array, as it's used in wrapped function
setResources(response.data);
// We call this function with the definite argument
})(resource);
// this callback doesn't return anything, it returns `undefined`
},
// our function depends on this argument, if resource is changed, `useEffect` will be invoked
[resource]
);
useEffect hook should receive a function, which can return another function to dispose of all dynamic data, listeners (e.g. remove event listeners, clear timeout callbacks, etc.)
Next example:
// this function returns a promise, but actually useEffect needs a function,
// which will be called to dispose of resources.
// To fix it, it's better to wrap this function
// (how it was implemented in the first example)
useEffect( async () => {
const response = await Axios('randomapi.com/word?key=*****&number={number_of_words}');
setData(response.data);
// we don't set up an array with dependencies.
// It means this `useEffect` will be called each time after the component gets rerendered.
// To fix it, it's better to define dependencies
});
So, we have 2 major errors:
1) Our useEffect callback returns a Promise instead of function, which implements dispose pattern
2) We missed dependencies array. By this reason component will call useEffect callback after each update.
Let's fix them:
useEffect(() => {// we wrap async function:
(async () => {
// we define resource directly in callback, so we don't have dependencies:
const response = await Axios('randomapi.com/word?key=*****&number={number_of_words}');
setData(response.data);
})()
}, []);
Finally, we have fixed our second example :)
I even made an example with custom fetchData hook and final version of useEffect: https://codesandbox.io/s/autumn-thunder-1i3ti
More about dependencies and refactoring hints for useEffect you can check here: https://github.com/facebook/react/issues/14920

Resources