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**
Related
Imagine I have a UI that has 2 controls: a date picker (which maps to a state date) and a drop down (which maps to a state filter)
I'm using useEffect in this React app to make an API call that uses the date and the filter. The code is set up like so:
const {date, filter} = state;
useEffect(() => {
const fetchData = async () => {
const result = await makeAPICallWithDate(date, filter); // API call here
// Update some UI state with the result
}
fetchData();
}, [date, filter]);
I have a < and > buttons that can change the date state pretty quickly. I notice that if I click very fast on these buttons, I get into a weird state where the whole thing is in an infinite loop and my server log shows that I'm making a bunch of API calls repeatedly.
I suspect that this is because my async function returns after the state has been updated. This doesn't happen if I click the < and > button slowly -- most likely because the async function has time to finish.
Questions:
Is my assumption correct?
How can I fix this so that nothing breaks when I spam the nav buttons?
One option is to debounce call, so you'll only make a single call while clicking fast.
You can use the debounce function that comes with lodash, create your own (see example here), or use a standalone module.
const {date, filter} = state;
const debounceFetch = useMemo(() => debounce(async (date, filter) => {
const result = await makeAPICallWithDate(date, filter); // API call here
// Update some UI state with the result
}, 300), [setUIState]);
useEffect(() => {
debounceFetch(date, filter);
}, [date, filter, debounceFetch]);
On return from useEffect you have to return function which will call AbortController
const {date, filter} = state;
useEffect(() => {
const controller = new AbortController();
let signal = controller.signal;
const fetchData = async () => {
return await fetch(url, {signal})
}
fetchData();
return () => {
controller.abort();
}
}, [date, filter]);
That way when you will call again useEffect then previous one will be canceled.
Debounce is good idea as well. but on slow network problem can still occur. So in my opinion you should combine this with debounce.
I'm trying to set a variable with a simple GET database call. The database call is returning the data correctly, but the variable remains undefined after every re-render. Code is below... the getMyThing() function in the useState() function is working correctly and returning the data I want.
import { getMyThing } from '../../utils/databaseCalls'
const MyComponent: React.FC = () => {
const { id } = useParams();
const [myThing, setMyThing] = useState(getMyThing(id));
useEffect(() => {
setMyThing(myThing)
}, [myThing]);
}
My thinking here was to use useState() to set the initial state of the myThing variable with the data returned from my database. I assume it's not immediately working since a database call is asynchronous, so I thought I could use useEffect() to update the myThing variable after the response of the database call completes, since that would trigger the useEffect() function because I have the myThing variable included as a dependency.
What am I missing here? Thanks!
EDIT: Thanks for the answers everyone, but I still can't get it to work by calling the getMyThing function asynchronously inside useEffect(). Is something wrong with my database call function? I guess it's not set up to a return a promise? Here's what that looks like:
export const getMyThing = (id) => {
axios.get('http://localhost:4000/thing/' + id)
.then(response => {
return(response.data);
})
.catch(function (error){
console.log(error);
})
}
You should do all your side effects(fetching data, subscriptions and such) in useEffect hooks and event handlers. Don't execute async logic in useState as you just assign the promise itself to the variable and not the result of it. In any case, it is a bad practice and it won't work. You should either:
import { getMyThing } from '../../utils/databaseCalls'
const MyComponent: React.FC = () => {
const { id } = useParams();
const [myThing, setMyThing] = useState(null);
useEffect(() => {
const fetchData = async () => {
const result = await getMyThing(id);
setMyThing(result);
};
fetchData();
}, [id, getMyThing]);
}
Or if you don't want to introduce an async function:
import { getMyThing } from '../../utils/databaseCalls'
const MyComponent: React.FC = () => {
const { id } = useParams();
const [myThing, setMyThing] = useState(null);
useEffect(() => {
getMyThing()
.then(result => setMyThing(result));
}, [id, getMyThing]);
}
Also, take note of the [id, getMyThing] part as it is important. This is a dependency array determining when your useEffect hooks are gonna execute/re-execute.
If getMyThing returns a Promise, the myThing will be set to that Promise on the first render, and then myThing will stay referring to that Promise. setMyThing(myThing) just sets the state to the Promise again - it's superfluous.
Call the asynchronous method inside the effect hook instead:
const [myThing, setMyThing] = useState();
useEffect(() => {
getMyThing(id)
.then(setMyThing);
}, []);
Here, myThing will start out undefined, and will be then set to the result of the async call as soon as it resolves.
You can't set the initial state with a value obtained asynchronously because you can't have the value in time.
myThing cannot both return the value you want and be asynchronous. Maybe it returns a promise that resolves to what you want.
Set an initial value with some default data. This might be null data (and later when you return some JSX from your component you can special case myThing === null by, for example, returning a Loading message).
const [myThing, setMyThing] = useState(null);
Trigger the asynchronous call in useEffect, much like you are doing now, but:
Make it rerun when the data it depends on changes, not when the data it sets changes.
Deal with whatever asynchronous mechanism your code uses. In this example I'll assume it returns a promise.
Thus:
useEffect(async () => {
const myNewThing = await getMyThing(id);
setMyThing(myNewThing)
}, [id]);
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.
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])
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
)
}