Using a read-once variable loaded async through react-hook - javascript

I'm currently playing around with Reacts hooks but currently I'm stuck at mixing different use-cases.
The following scenario is what I am trying to get working. There should be one hook called useNationsAsync which is retrieving a list of available nations from the server.
Inside the hook I check if the list has already been loaded/stored to the localStorage in order to load it only once.
For the remote-call I use axios' get call together with the await keyword. So this "hook" has to be async. I've implemented it the following:
export async function getNationsAsync({ }: IUseNationsProps = {}): Promise<NationResource[]> {
const [storedNations, setStoredNations] = useLocalStorage<NationResource[]>("nations", null);
if (storedNations == null) {
const nationsResponse = await Axios.get<NationsGetResponse>("/v1/nations/");
setStoredNations(nationsResponse.data.nations);
}
return storedNations;
}
The useLocalStorage-hook is the one which can be found here (only typed for use with TypeScript).
In my final FunctionalComponent I only want to read the nations once so I thought using the useEffect hook with an empty array would be the place to be (as this is mainly the same as componentDidMount).
However, on runtime I get the following error on the first line of my getNationsAsync-hook:
Uncaught (in promise) Invariant Violation: Invalid hook call.
The usage in my FunctionalComponent is:
const [nations, setNations] = React.useState<NationResource[]>([]);
const fetchNations = async () => {
const loadedNations = await getNationsAsync();
setNations(loadedNations);
};
React.useEffect(() => {
fetchNations();
}, []);
I know that the issue is for calling useHook inside the method passed to useEffect which is forbidden.
The problem is, that I don't get the right concept on how to use the nations at a central point (a hook sharing the result, not the logic) but only load them once in the components which do need nations.

The hook you are creating manages the state of nations and returns it.
Instead of useState you are using useLocalStorage which, as far as I could read from the source, uses as initial state a localStorage value or the given value (null in your case) if there is no local one.
export const useNations = ():
| NationResource[]
| null => {
const [storedNations, setStoredNations] = useLocalStorage<
NationResource[] | null
>("nations", null);
useEffect(() => {
// If storedNations has a value don't continue.
if (storedNations) {
return;
}
const fetch = async () => {
// Check the types here, Im not sure what NationsGetResponse has.
const nationsResponse = await Axios.get<NationsGetResponse>(
"/v1/nations/"
);
setStoredNations(nationsResponse.data.nations);
};
fetch();
}, []);
return storedNations;
};
Then you can use this hook in any component:
export const MyComponent: React.ComponentType<Props> = props => {
const nations = useNations();
return // etc.
};

You can add async to custom hooks. I think this is what you're looking for.
export function getNations(props) {
const [storedNations, setStoredNations] = useLocalStorage("nations",[]);
async function getNations() {
const nationsResponse = await Axios.get("/v1/nations/");
setStoredNations(nationsResponse.data.nations);
}
useEffect(() => {
if (storedNations.length === 0) {
getNations()
}
}, [])
return storedNations;
}
You can call it like this
function App() {
const nations = getNations()
return (
// jsx here
)
}

Related

Struggling with async custom react hooks. How do i await result of one hook to use in another hook?

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!
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,
}
)
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
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.

Why does this async function not return anything?

I have a function that needs to run when my app starts. It basically makes several API calls to fetch the user's information, count its messages and subscribe to a socket channel.
It is made with React-query. But it doesn't perform any API call. The function _getUserInformations is never triggered and the network tab of my inspector remains void.
The server has no issues and the routes work. Here is the code:
export default function App() {
useEffect(() => {
initializeUser();
}, []);
const user = userStore();
return routes + app
);
}
export const initializeUser = () => {
try {
const res = await getUserInformations();
const user = res.data.user;
updateUser(user);
const { unreadConversations } = await hasUnreadConversations(user._id);
updateunreadConversations(unreadConversations);
getNotifications(user._id);
}
catch (err) {
return null;
}
};
const _getUserInformations = async () => {
try {
const userToken = await api.get("/user-informations", {
withCredentials: true,
});
return userToken;
} catch (err) {
throw new Error(err.message || "error.unknown");
}
};
const getUserInformations: UserService["getUserInformations"] = () => {
const { data } = useQuery("getUserInfos", () => _getUserInformations(), {
cacheTime: 1000 * 60 * 60 * 24, // 24 hours
});
return data.data.user;
};
What is wrong here?
Here is also a sandbox that reproduces the same issue with PokeApi: https://codesandbox.io/s/gifted-hill-tei88?file=/src/App.js
_getPokemon (that imitates _getUserInformations) is never called as well...
Hooks should only be used in a functional component.
For example in your codesandbox codes, if you use the following, it will work.
export default function App() {
const { data, isError } = useQuery("getpokemon", () => _getPokemon());
console.log('data', data) //you can see the data printed.
React.useEffect(() => initialize(), []);
if (!data && !isError) return <Loading /> // Bonus Tip - data is fetching if both data and isError is undefined.
return <div>hello</div>;
}
https://reactjs.org/docs/hooks-rules.html
Don’t call Hooks from regular JavaScript functions. Instead, you can:
✅ Call Hooks from React function components.
✅ Call Hooks from custom
Hooks (we’ll learn about them on the next page).
Also...
Only Call Hooks at the Top Level Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top
level of your React function, before any early returns. By following
this rule, you ensure that Hooks are called in the same order each
time a component renders. That’s what allows React to correctly
preserve the state of Hooks between multiple useState and useEffect
calls. (If you’re curious, we’ll explain this in depth below.)

useState with async function returning Promise {<pending>}

So, I know this question has been asked 100's of times, but none of the solutions seems to work in my instance.
I am using useState hook to update state to a value initialValues that gets data returned from a getInitialValues function
const [initialValues, setInitialValues] = useState(getInitialValues());
The getInitialValues function does a logic check and either returns an object or another function retrieveDetails()
const getInitialValues = () => {
let details;
if(!addressDetails) {
details = retrieveDetails();
} else {
details = {
...,
...,
...
};
}
return details;
}
The function, retrieveDetails is an async function that makes an API call, and I await the response and return the object received from the response.
const retrieveDetails = async () => {
const addr = addressDetails[currentAddress];
const { addressLookup } = addr;
const key = process.env.API_KEY;
const query = `?Key=${key}&Id=${addressLookup}`;
const addrDetails = await new AddrService().getAddressDetails(query);
return addrDetails;
}
However, when I log the state initialValues it returns Promise {<pending>}?
Even removing the API call and simple returning an object in it's place renders the same result.
Not sure the best way around this to actually return the object?
Any help would be greatly appreciated.
I don't think there is a way to get initial data to useState asynchronously, at least not yet.
React is not waiting for your data to arrive, the function will keep on running to completion while your async operation is queued (on the event loop side).
The current idiomatic way is to fetch the data in an effect and update the state.
useEffect(() => {
getData(someParam).then(data => setState(data))
}, [someParam])
You can read more about it in the DOCS
This isn't something React's built-in hooks support.
You need to build or import a custom hook.
Want short and simple? Try this:
const addressDetails = useAsyncFunctionResult(retrieveDetails);
and add this in hooks/useAsyncFunctionResults.js
function useAsyncFunctionResult(asyncFunction, dependencies = []) {
const [result, setResult] = React.useState();
React.useEffect(() => {
let mounted = true;
asyncFunction().then((data) => mounted && setResult(data))
return () => { mounted = false; }
}, dependencies]);
return result;
}
Here the retrieveDetails function (the one from the question) will start executing (the ().then bit above) when the hook is first called. The result is kept until unmount. And we get no errors about changing component state after unmounting.
If you later want to add caching, use existing hooks instead if making your own.
There's no official useAsync in React, and likely never will be, because if your Promise did a request and the requesting component did unmount before the promise resolves, then best practice is to cancel which differs case by case.
const retrieveDetails = async () => {
const addr = addressDetails[currentAddress];
const { addressLookup } = addr;
const key = process.env.API_KEY;
const query = `?Key=${key}&Id=${addressLookup}`;
const addrDetails = await Promise.resolve(new AddrService().getAddressDetails(query))
return addrDetails;
}
**try this once changed the function a bit**

How do I access the latest value of state using useSelector after dispatching an action in redux?

I have a screen in a React-Native project which essentially just renders a loading icon whilst fetching data from the server, before then taking the user to the main screen. The first function getPrivateKey() will return the private key and store it using redux in the state, and the next function connectWithKey() will then use that key to connect.
The issue I'm facing is that when connectWithkey() runs, it's using the initial, empty value of the private key, not the updated value. Here's the code, and apologies if I'm being stupid it's been a long day :(
export default DataLoader = props => {
//private key - this should, in theory, update after getPrivateKey()
const privateKey = useSelector(({ main }) => main.privateKey);
const dispatch = useDispatch();
useEffect(() => {
const configure = async () => {
//this will update the private key
await getPrivateKey();
//this should use the new private key from useSelector, but instead is using the initialised empty object
await connectWithKey();
props.navigation.navigate('MainScreen');
};
configure();
}, []);
//.... more code below....
I've tried adding privateKey into the array dependencies which just caused an infinite loop, and I've checked that the value has updated in the redux store - so I'm a bit lost! In essence, it appears that the useSelector hook isn't getting a fresh value. Any help would be very much appreciated 😊 Thanks!
EDIT - added more code upon request 😊
const getPrivateKey = async () => {
const privKey = await fetchKeyFromServer();
dispatch({
type: 'UPDATE',
value: privKey
});
};
const connectWithkey = async () => {
//the privateKey here should be the updated value from useSelector
await connectToServer(privateKey)
};
Looks like your getPrivateKey function is a thunk, but you are not dispatching it ? And there is nothing stopping you from returning values from thunks.
const getPrivateKey = async (dispatch) => {
const privKey = await fetchKeyFromServer();
dispatch({
type: 'UPDATE',
value: privKey
});
return privKey // return the key here to whoever wants to use the value immediately.
};
Then in your useEffect in the component you can use the return value easily :)
useEffect(() => {
const configure = async () => {
//make sure you 'dispatch' this thunk
const key = await dispatch(getPrivateKey());
// pass the key
await dispatch(connectWithKey(key));
...
};
....
}, []);
The code above assumes that the connectWithKey is a thunk too. If so, you can design the thunk in a way that it either uses the passed value or reads it from the redux store.
const connectWithkey = (privateKey: passedPrivateKey) = async (dispatch, getState) => {
const state = getState();
let privateKey = state.whatever.the.path.is.to.privateKey;
// use the passed private key if it is present.
if (passedPrivateKey) {
privateKey = passedPrivateKey;
}
await connectToServer(privateKey)
};
I have used this approach several times in my app. This way you do not need to rely on the state in the selector. And should you choose to rely on that state, the dependencies of your useEffect should update accordingly. Right now it is an empty array, and that is why the effect doesn't run again on any state changes (it is acting like the componentDidMount lifecycle function).
const privateKey = useSelector(({ main }) => main.privateKey);
useEffect(() => {
await getPrivateKey();
if (privateKey) {
await connectWithKey();
}
}, [privateKey]);
This way your hook re-runs everytime privateKey state changes. You might need to have some sort of condition for your connectWithKey thunk though, so that it doesn't run if the key is null.

React hooks: what is best practice for wrapping multiple instances of a hook with a single hook?

I have a react hook useDbReadTable for reading data from a database that accepts initial data of tablename and query. It returns an object that includes an isLoading status in addition to the data from the database.
I want to wrap this hook in a new hook that accepts initial data of an array of { tablename, query }, and returns an object with the data from the database for each table, but with the isLoading statuses consolidated into a single boolean based on logic in my new hook.
The idea is, the caller of the new hook can ask for data from a number of tables, but only has to check one status value.
My thought was to have the new hook look something like,
EDIT: Updated code (I had pasted the wrong version)
export const useDbRead = tableReads => {
let myState = {};
for (let i = 0; i < tableReads.length; ++i) {
const { tablename, query = {} } = tableReads[i];
const [{ isLoading, isDbError, dbError, data }] = useDbReadTable(tablename, query);
myState = { ...myState, [tablename]: { isLoading, isDbError, dbError, data }};
}
const finalState = {
...myState,
isLoading: Object.values(myState).reduce((acc, t) => acc || t.isLoading, false),
};
return [finalState];
};
However, eslint gives me this error on my useDbReadTable call:
React Hook "useDbReadTable" may be executed more than once. Possibly because it is called in a loop. React Hooks must be called in the exact same order in every component render. react-hooks/rules-of-hooks
And Rules for Hooks says,
Only Call Hooks at the Top Level
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls. (If you’re curious, we’ll explain this in depth below.)
After reading the rule and the explanation, it seems the only issue is making sure the hooks are called in the same order on all re-renders. As long as I ensure the list of tables I pass in to my new hook never changes, shouldn't my new hook work fine (as my initial tests indicate)? Or am I missing something?
More importantly, is there a better idea how to implement this, that doesn't violate the Rules of Hooks?
Edit2: in case its helpful, here's useDbReadTable. Note that it includes more functionality than I mention in my question, since I wanted to keep the question as simple as possible. My question is whether my useDbRead is a good solution, or is there a good way to do it without violating the Rules of Hooks?
export const useDbReadTable = (initialTableName, initialQuery = {}, initialData = []) => {
const dbChangeFlag = useSelector(({appState}) => appState.dbChangeFlag);
const [tableName, setTableName] = useState(initialTableName);
const [query, setQuery] = useState(initialQuery);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isDbError: false,
dbError: {},
data: initialData,
});
useEffect(() => {
let didCancel = false;
const fetchData = async () => {
dispatch({ type: dataFetch.FETCH_INIT });
try {
const result = Array.isArray(query) ?
await db[tableName].batchGet(query) // query is an array of Ids
:
await db[tableName].find(query);
if (!didCancel) {
dispatch({ type: dataFetch.FETCH_SUCCESS, payload: result });
}
} catch (error) {
if (!didCancel) {
dispatch({ type: dataFetch.FETCH_FAILURE, payload: error });
}
}
};
fetchData().then(); // .then() gets rid of eslint warning
return () => {
didCancel = true;
};
}, [query, tableName, dbChangeFlag]);
return [state, setQuery, setTableName];
};
You can probably avoid using the useDbReadSingle by making useDbRead itself array aware. Something like:
export const useDbRead = tableReads => {
const [loading, setLoading] = useState(true);
useEffect(() => {
const doIt = async () => {
// you would also need to handle the error case, but you get the idea
const data = await Promise.all(
tableReads.map(tr => {
return mydbfn(tr);
})
);
setLoading(false);
};
doIt();
}, [tableReads]);
return { loading, data };
};
When you need to use it for single table read, just call this with a array that has single element.
const {loading, data: [d]} = useDbRead([mytableread])

Categories