how is it difference between queryFunction in react-query? - javascript

hi i'm beginner of react and react-query.
below code,
it is correctly working!!
const { data, isLoading, isError, error } = useQuery("comments", () => {
return fetchComments(post.id);
});
and it is not working.
const { data, isLoading, isError, error } = useQuery("comments", fetchComments(post.id))
What is the difference between these?

In first case, you are giving a function that will call fetchComments as a callback function. react-query will take that function and call it, which will call fetchComments
in second case, you are immidietaly running fetchComments function and passing returned value as param, and react-query is trying to run whatever fetchComments will return, which i assume is Promise and not a function
under some specific circumstances, you can just pass the function refernce without calling it, but you will be unable to pass any props:
const { data, isLoading, isError, error } = useQuery("comments", fetchAllComments)
Your first solution is the correct one.
and suggestion, you can use Arrow function without return, for me its more readable
const { data, isLoading, isError, error } = useQuery(["comments", post.id], () => fetchComments(post.id));

Related

Cannot call useQuery inside event handler (react-query, react-hook-form)

I have to call useQuery inside event handler. (user registration) and Im trying something like this.
const [postUser, { isError, isLoading, data }] = useQuery('user', () =>
axios.post(API_URL, { query: GET_TOKEN_QUERY }).then((res) => res.data)
)
const openModalCallback = React.useCallback(
(modalName) => {
onOpenModal(modalName)
},
[onOpenModal]
)
async function onSubmit(data: LoginInputs) {
if (data.checkbox) {
postUser()
}
}
But I get an error like this and cant resolve it. Help me please.
If you want to create/update/delete data or perform server side-effects (opposed to just read data from server), better use useMutation.
The hook returns a mutate() function which you can call onSubmit().
You cannot simply post the data with the useQuery hook, for that you have to use the useMutation() hook and it returns a function which you can call inside an event handler.

Apollo-client useLazyQuery returning undefined on refetch

I have a subcomponent Viewer that uses a refetch function passed down to its parent Homescreen.
The lazyQuery in homescreen is structured as follows:
const [getById, {loading, error, data, refetch}] = useLazyQuery(GET_BY_ID);
This will get an object from my mongoDB by its id, and when I need to call it again and reload data into my custom activeObject variable, I use the follow function:
const refetchObjects= async () => {
const {loading, error, data } = await refetch();
if (error) { console.log(error);}
if (data) {
activeObject = data.getRegionById;
}
}
However, sometimes the return object of await refetch(); is undefined and I'm not sure why.

How to use an async database call to set a variable with useState() and useEffect()?

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]);

Setting a useEffect hook's dependency within`useEffect` without triggering useEffect

Edit: It just occurred to me that there's likely no need to reset the variable within the useEffect hook. In fact, stateTheCausesUseEffectToBeInvoked's actual value is likely inconsequential. It is, for all intents and purposes, simply a way of triggering useEffect.
Let's say I have a functional React component whose state I initialize using the useEffect hook. I make a call to a service. I retrieve some data. I commit that data to state. Cool. Now, let's say I, at a later time, interact with the same service, except that this time, rather than simply retrieving a list of results, I CREATE or DELETE a single result item, thus modifying the entire result set. I now wish to retrieve an updated copy of the list of data I retrieved earlier. At this point, I'd like to again trigger the useEffect hook I used to initialize my component's state, because I want to re-render the list, this time accounting for the newly-created result item.
​
const myComponent = () => {
const [items, setItems] = ([])
useEffect(() => {
const getSomeData = async () => {
try {
const response = await callToSomeService()
setItems(response.data)
setStateThatCausesUseEffectToBeInvoked(false)
} catch (error) {
// Handle error
console.log(error)
}
}
}, [stateThatCausesUseEffectToBeInvoked])
const createNewItem = async () => {
try {
const response = await callToSomeService()
setStateThatCausesUseEffectToBeInvoked(true)
} catch (error) {
// Handle error
console.log(error)
}
}
}
​
I hope the above makes sense.
​
The thing is that I want to reset stateThatCausesUseEffectToBeInvoked to false WITHOUT forcing a re-render. (Currently, I end up calling the service twice--once for win stateThatCausesUseEffectToBeInvoked is set to true then again when it is reset to false within the context of the useEffect hook. This variable exists solely for the purpose of triggering useEffect and sparing me the need to elsewhere make the selfsame service request that I make within useEffect.
​
Does anyone know how this might be accomplished?
There are a few things you could do to achieve a behavior similar to what you described:
Change stateThatCausesUseEffectToBeInvoked to a number
If you change stateThatCausesUseEffectToBeInvoked to a number, you don't need to reset it after use and can just keep incrementing it to trigger the effect.
useEffect(() => {
// ...
}, [stateThatCausesUseEffectToBeInvoked]);
setStateThatCausesUseEffectToBeInvoked(n => n+1); // Trigger useEffect
Add a condition to the useEffect
Instead of actually changing any logic outside, you could just adjust your useEffect-body to only run if stateThatCausesUseEffectToBeInvoked is true.
This will still trigger the useEffect but jump right out and not cause any unnecessary requests or rerenders.
useEffect(() => {
if (stateThatCausesUseEffectToBeInvoked === true) {
// ...
}
}, [stateThatCausesUseEffectToBeInvoked]);
Assuming that 1) by const [items, setItems] = ([]) you mean const [items, setItems] = useState([]), and 2) that you simply want to reflect the latest data after a call to the API:
When the state of the component is updated, it re-renders on it's own. No need for stateThatCausesUseEffectToBeInvoked:
const myComponent = () => {
const [ items, setItems ] = useState( [] )
const getSomeData = async () => {
try {
const response = await callToSomeService1()
// When response (data) is received, state is updated (setItems)
// When state is updated, the component re-renders on its own
setItems( response.data )
} catch ( error ) {
console.log( error )
}
}
useEffect( () => {
// Call the GET function once ititially, to populate the state (items)
getSomeData()
// use [] to run this only on component mount (initially)
}, [] )
const createNewItem = async () => {
try {
const response = await callToSomeService2()
// Call the POST function to create the item
// When response is received (e.g. is OK), call the GET function
// to ask for all items again.
getSomeData()
} catch ( error ) {
console.log( error )
}
} }
However, instead of getting all items after every action, you could change your array locally, so if the create (POST) response.data is the newly created item, you can add it to items (create a new array that includes it).

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