In my React project using React Query, I have a functional component MoveKeywordModal such that:
when it first loads, it fetches from API endpoint api/keyword_lists to fetch a bunch of keywordLists data. For each of these keywordLists, call it list, I create a clickable element.
When the clickable element (wrapped in a HoverWrapper) gets clicked, I want to send a POST API request to api/keyword_lists/:list_id/keyword_list_items/import with some data.
where :list_id is the id of the list just clicked.
export const MoveKeywordModal = ({
setShowMoveKeywordModal,
keywordsToMove
}) => {
const { data: keywordLists } = useQuery('api/keyword_lists', {})
const [newKeywordList, setNewKeywordList] = useState({})
const { mutate: moveKeywordsToList } = useMutation(
`api/keyword_lists/${newKeywordList.id}/keyword_list_items/import`,
{
onSuccess: data => {
console.log(data)
},
onError: error => {
console.log(error)
}
}
)
const availableKeywordLists = keywordLists
.filter(l => l.id !== activeKeywordList.id)
.map(list => (
<HoverWrapper
id={list.id}
onClick={() => {
setNewKeywordList(list)
moveKeywordsToList({
variables: { newKeywordList, data: keywordsToMove }
})
}}>
<p>{list.name}</p>
</HoverWrapper>
))
return (
<>
<StyledModal
isVisible
handleBackdropClick={() => setShowMoveKeywordModal(false)}>
<div>{availableKeywordLists}</div>
</StyledModal>
</>
)
}
Despite calling setNewKeywordList(list) in the onClick of the HoverWrapper, it seems the newKeywordList.id is still not defined, not even newKeywordList is defined.
What should I do to fix it?
Thanks!
react doesn’t perform state updates immediately when you call the setter of useState - an update is merely 'scheduled'. So even though you call setNewKeywordList, the newKeywordList will not have the new value in the next line of code - only in the next render cycle.
So while you are in your event handler, you’ll have to use the list variable:
setNewKeywordList(list)
moveKeywordsToList({
variables: { newKeywordList: list, data: keywordsToMove }
})
/edit: I just realized that your call to useMutation is not correct. It doesn’t have a key like useQuery, it has to provide a function as the first argument that takes variables, known as the mutation function:
const { mutate: moveKeywordsToList } = useMutation(
(variables) => axios.post(`api/keyword_lists/${variables.newKeywordList.id}/keyword_list_items/import`),
{
onSuccess: data => {
console.log(data)
},
onError: error => {
console.log(error)
}
}
)
see also: https://react-query.tanstack.com/guides/mutations
Related
In my project, I am trying to redirect to the listed page after updating an item. The code is working properly but here I am facing an issue, the loader is not working.
export const useUpdateStatusArchiveSurvey = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateArchiveSurvey,
onSuccess: () => {
queryClient.invalidateQueries(['searched-public-survey']);
},
});
};
By using "invalidateQueries" the updated values are displayed in the list but the loader is not working.
...
...
const {
data: queriedSurvey,
fetchNextPage: fetchNextQueries,
isLoading,
} = useListAllPublicSurvey({
query: search,
status: tab,
orderDesc: orderDesc,
actionPlanId: actionValue?.id,
});
useEffect(() => {
fetchNextQueries();
}, [queriedSurvey, search, tab, orderDesc, actionValue]);
const querySurvey = useMemo(
() =>
queriedSurvey?.pages
.map((page) => page.edges.map((edge: object) => edge))
.flat(),
[queriedSurvey, search]
);
...
...
const queryPlans = useMemo(
() =>
queriedPlans?.pages
.map((page) => page.edges.map((edge: object) => edge))
.flat(),
[queriedPlans, actionSearch]
);
const onChange = (e: any) => {
setActionValue(e);
};
console.log("isLoading", isLoading);
if (isLoading) {
return <Glimmer open={isLoading} />;
}
return (
....
....
when I console the "isLoading" at the initial call it is "true" otherwise it is "false" always.
React-query has several flags in the object returned by the useQuery hook. Note that isLoading will only be true if there is no data and the query is currently fetching. Since you already have data and you invalidated it, the stale data will be present until the refetch is complete. Use the isFetching flag to determine if a fetching is in progress regardless of having stale data or not.
I am trying to extract my API calls using react-query into a reusable hook. The parameters I need to send to this hook are moduleName and value. For some reason, I get an error that I need to follow hooks rules.
Please advice.
This is my code:
export const useAutoSave = () => {
const fetcher = useCallback(
(
moduleName: ISourceLoaderEditTabs,
value: Partial<ISourceConfigurationEdit[ISourceLoaderEditTabs]>,
saveUrl = '',
) => {
const handleSaveSourceDetailsMutation = useMutation(
(data: ISourceConfigurationEdit) =>
saveUrl
? postSaveStageRaw(`${POST_SAVE_STAGE_RAW}?${saveUrl}`, data)
: saveSourceDetails(data),
);
const sourceId = sessionStorage.getItem('sourceId');
const sourceDetail = queryClient.getQueryData([
'getSourcesDetail',
Number(sourceId),
]);
handleSaveSourceDetailsMutation.mutate(
{
...(sourceDetail as ISourceConfigurationEdit),
[moduleName]: {
...(sourceDetail as ISourceConfigurationEdit)[moduleName],
...value,
},
},
{
onSuccess: async (data) => {
queryClient.setQueryData(
['getSourcesDetail', Number(sourceId)],
data,
);
},
},
);
},
[],
);
return [fetcher];
};
Then in my component I use it as
const [fetch] = useAutoSave();
fetch('abc', {
name:'a2441918'
})
Code snippet : Stackblitz: https://stackblitz.com/edit/react-q8uvse?file=src%2Fhooks.js
you cannot call useMutation inside useCallback. Also, you don't need to. useMutation returns one object with two functions - mutate and mutateAsync, that you can invoke when you want to call invoke your mutation. So your custom hook very likely should only return whatever useMutation returns. The fist argument to useMutation is the mutateFn - the function that is called when you invoke mutate or mutateAsync, and you can also pass one parameters object there:
const useAutoSave = () => {
return useMutation(
({ moduleName, value, saveUrl }) => saveUrl
? postSaveStageRaw(`${POST_SAVE_STAGE_RAW}?${saveUrl}`, data)
: saveSourceDetails(data),
)
}
you can then invoke it via:
const { mutate } = useAutoSave()
<button onClick={() => {
mutate({ moduleName: 'something, value: 'somethingElse' })
}}>Save</button>
The issue as it states in the error log - usage of useMutation
const handleSaveSourceDetailsMutation = useMutation(
(data: ISourceConfigurationEdit) =>
saveUrl
? postSaveStageRaw(`${POST_SAVE_STAGE_RAW}?${saveUrl}`, data)
: saveSourceDetails(data),
);
This useMutation hook needs to be outside of the useCallback. This also means that the saveUrl and other params need to be refactored.
export const useAutoSave = () => {
const i_dont_know = useMutation(x,x,x,x); // hooks can't be called in regular functions
}
Rules of hook for reference: https://reactjs.org/docs/hooks-rules.html
I'm quite new to React and I don't always understand when I have to use hooks and when I don't need them.
What I understand is that you can get/set a state by using
const [myState, setMyState] = React.useState(myStateValue);
So. My component runs some functions based on the url prop :
const playlist = new PlaylistObj();
React.useEffect(() => {
playlist.loadUrl(props.url).then(function(){
console.log("LOADED!");
})
}, [props.url]);
Inside my PlaylistObj class, I have an async function loadUrl(url) that
sets the apiLoading property of the playlist to true
gets content
sets the apiLoading property of the playlist to false
Now, I want to use that value in my React component, so I can set its classes (i'm using classnames) :
<div
className={classNames({
'api-loading': playlist.apiLoading
})}
>
But it doesn't work; the class is not updated, even if i DO get the "LOADED!" message in the console.
It seems that the playlist object is not "watched" by React. Maybe I should use react state here, but how ?
I tested
const [playlist, setPlaylist] = React.useState(new PlaylistObj());
React.useEffect(() => {
//refresh playlist if its URL is updated
playlist.loadUrl(props.playlistUrl).then(function(){
console.log("LOADED!");
})
}, [props.playlistUrl]);
And this, but it seems more and more unlogical to me, and, well, does not work.
const [playlist, setPlaylist] = React.useState(new PlaylistObj());
React.useEffect(() => {
playlist.loadUrl(props.playlistUrl).then(function(){
console.log("LOADED!");
setPlaylist(playlist); //added this
})
}, [props.playlistUrl]);
I just want my component be up-to-date with the playlist object. How should I handle this ?
I feel like I'm missing something.
Thanks a lot!
I think you are close, but basically this issue is you are not actually updating a state reference to trigger another rerender with the correct loading value.
const [playlist, setPlaylist] = React.useState(new PlaylistObj());
React.useEffect(() => {
playlist.loadUrl(props.playlistUrl).then(function(){
setPlaylist(playlist); // <-- this playlist reference doesn't change
})
}, [props.playlistUrl]);
I think you should introduce a second isLoading state to your component. When the effect is triggered whtn the URL updates, start by setting loading true, and when the Promise resolves update it back to false.
const [playlist] = React.useState(new PlaylistObj());
const [isloading, setIsLoading] = React.useState(false);
React.useEffect(() => {
setIsLoading(true);
playlist.loadUrl(props.playlistUrl).then(function(){
console.log("LOADED!");
setIsLoading(false);
});
}, [props.playlistUrl]);
Use the isLoading state in the render
<div
className={classNames({
'api-loading': isLoading,
})}
>
I also suggest using the finally block of a Promise chain to end the loading in the case that the Promise is rejected your UI doesn't get stuck in the loading "state".
React.useEffect(() => {
setIsLoading(true);
playlist.loadUrl(props.playlistUrl)
.then(function() {
console.log("LOADED!");
})
.finally(() => setIsLoading(false));
}, [props.playlistUrl]);
Here you go:
import React from "react";
class PlaylistAPI {
constructor(data = []) {
this.data = data;
this.listeners = [];
}
addListener(fn) {
this.listeners.push(fn);
}
removeEventListener(fn) {
this.listeners = this.listeners.filter(prevFn => prevFn !== fn)
}
setPlayList(data) {
this.data = data;
this.notif();
}
loadUrl(url) {
console.log("called loadUrl", url, this.data)
}
notif() {
this.listeners.forEach(fn => fn());
}
}
export default function App() {
const API = React.useMemo(() => new PlaylistAPI(), []);
React.useEffect(() => {
API.addListener(loadPlaylist);
/**
* Update your playlist and when user job has done, listerners will be called
*/
setTimeout(() => {
API.setPlayList([1,2,3])
}, 3000)
return () => {
API.removeEventListener(loadPlaylist);
}
}, [API])
function loadPlaylist() {
API.loadUrl("my url");
}
return (
<div className="App">
<h1>Watching an object by React Hooks</h1>
</div>
);
}
Demo in Codesandbox
I am new to this react-query library.
I know that when I want to fetch data, with this library I can do something like this:
const fetchData = async()=>{...}
// it starts fetching data from backend with this line of code
const {status, data, error} = useQuery(myKey, fetchData());
It works. But how to trigger the data fetching only when a button is clicked? , I know I probably could do something like <Button onPress={() => {useQuery(myKey, fetchData())}}/> , but how to manage the returned data and status...
According to the API Reference, you need to change the enabled option to false to disable a query from automatically running. Then you refetch manually.
// emulates a fetch (useQuery expects a Promise)
const emulateFetch = _ => {
return new Promise(resolve => {
resolve([{ data: "ok" }]);
});
};
const handleClick = () => {
// manually refetch
refetch();
};
const { data, refetch } = useQuery("my_key", emulateFetch, {
refetchOnWindowFocus: false,
enabled: false // disable this query from automatically running
});
return (
<div>
<button onClick={handleClick}>Click me</button>
{JSON.stringify(data)}
</div>
);
Working sandbox here
Bonus: you can pass anything that returns a boolean to enabled.
That way you could create Dependant/Serial queries.
// Get the user
const { data: user } = useQuery(['user', email], getUserByEmail)
// Then get the user's projects
const { isIdle, data: projects } = useQuery(
['projects', user.id],
getProjectsByUser,
{
// `user` would be `null` at first (falsy),
// so the query will not execute until the user exists
enabled: user,
}
)
You have to pass the manual: true parameter option so the query doesn't fetch on mount. Also, you should pass fetchData without the parentheses, so you pass the function reference and not the value.
To call the query you use refetch().
const {status, data, error, refetch} = useQuery(myKey, fetchData, {
manual: true,
});
const onClick = () => { refetch() }
Refer to the manual querying section on the react-query docs for more info
https://github.com/tannerlinsley/react-query#manual-querying
Looks like the documentation changed and is missing the manual querying section right now. Looking at the useQuery API however, you'd probably need to set enabled to false, and then use refetch to manually query when the button is pressed. You also might want to use force: true to have it query regardless of data freshness.
You can try this version:
const fetchData = async()=>{...}
// it starts fetching data from backend with this line of code
const {status, data, error, refetch } = useQuery(
myKey,
fetchData(),
{
enabled: false,
}
);
const onClick = () => { refetch() }
// then use onClick where you need it
From documentation Doc:
enabled: boolean
Set this to false to disable this query from automatically running.
Can be used for Dependent Queries.
refetch: (options: { throwOnError: boolean, cancelRefetch: boolean }) => Promise<UseQueryResult>
A function to manually refetch the query.
If the query errors, the error will only be logged. If you want an error to be thrown, pass the throwOnError: true option
If cancelRefetch is true, then the current request will be cancelled before a new request is made
There is another way to do this that also works if you want to trigger multiple refetches.
const [fetch, setFetch] = useState(null);
const query = useQuery(["endpoint", fetch], fetchData);
const refetch = () => setFetch(Date.now());
// call the refetch when handling click.
If you want to refetch multiple entities you could have a top level useState that is called for instance fetchAll and:
...
const query = useQuery(["endpoint", fetch, fetchAll], fetchData);
...
and this code will also trigger if you press a button to fetch all.
At first react query gives us enabled option and by default it is true
const fetchData = async()=>{...}
const {status, data, error , refetch} = useQuery(myKey, fetchData() , {
enabled : false
}
);
<button onClick={() => refetch()}>Refetch</button>
If the key is the same, then use refetch(), if the key is different then use useState to trigger the query.
For example:
const [productId, setProductId] = useState<string>('')
const {status, data, error, refetch} = useQuery(productId, fetchData, {
enable: !!productId,
});
const onClick = (id) => {
if(productId === id) {
refetch()
}
else {
setProductId(id)
}
}
you can use useLazyQuery()
import React from 'react';
import { useLazyQuery } from '#apollo/client';
function DelayedQuery() {
const [getDog, { loading, error, data }] = useLazyQuery(GET_DOG_PHOTO);
if (loading) return <p>Loading ...</p>;
if (error) return `Error! ${error}`;
return (
<div>
{data?.dog && <img src={data.dog.displayImage} />}
<button onClick={() => getDog({ variables: { breed: 'bulldog' } })}>Click me!</button>
</div>
);
}
reference: https://www.apollographql.com/docs/react/data/queries/#manual-execution-with-uselazyquery
let's I have a graphql mutation component, that I reuse in many places
const MarkAsViewed =({ type = 1, children }) => {
const markAsViewed = (commitMutation) => (type) => {
return commitMutation({
variables: { type }
});
};
return (
<MarkAsViewedMutation
mutation={MARK_AS_VIEWED_MUTATION}
variables={{
type,
}}
>
{
(commitMutation, { error, loading }) => children({
markAsViewed: markAsViewed(commitMutation)
})
}
</MarkAsViewedMutation>
);
};
however since markAsViewed is a closure function, it will always return different function with different ref which means different for react.
this makes the child component to have to do a useCallback like:
const alwaysSameRefFunc = useCallback(()=>{ markAsViewed(), []}
above works but creates 2 problems:
I get linter warning saying I should add markAsViewed as dependency blah blah. which I cannot, because it triggers infinite loop (since it's different ref every time)
everyone that uses <MarkAsViewed /> component will need to manually memoirzation
Ideally this is what I want, but it's an invalid code, because "markAsViewed" is not a react component and cannot have useCallback
const markAsViewed = (commitMutation) => useCallback((type) => {
return commitMutation({
variables: { type }
});
}, []);
any idea how can I solve the issue?
note: we are not ready to update Apollo version to have hoook yet
Does the following work?
const markAsViewed = commitMutation => type => {
return commitMutation({
variables: { type },
});
};
const MarkAsViewed = ({ type = 1, children }) => {
const fn = useCallback(
(commitMutation, { error, loading }) =>
children({
markAsViewed: markAsViewed(commitMutation),
}),
[children]
);
return (
<MarkAsViewedMutation
mutation={MARK_AS_VIEWED_MUTATION}
variables={{
type,
}}
>
{fn}
</MarkAsViewedMutation>
);
};
I'm not sure if that will work because it still depends on children, if that causes unnesesary renders then maybe post how MarkAsViewed component is rendered.