I'm trying to refetch some queries after one success but it's not working!
I used two ways to handle it by using refetchQueries() / invalidateQueries()
1- onSuccess callback
export const useMutateAcceptedOrder = () => {
const queryClient = useQueryClient();
return useMutation(
['AcceptedOrder'],
(bodyQuery: AcceptedOrderProps) => acceptOrder(bodyQuery),
{
onSuccess: () => {
console.log('success, refetch now!');
queryClient.invalidateQueries(['getNewOrders']); // not work
queryClient.refetchQueries(['getNewOrders']); // not work
},
onError: () => {
console.error('err');
queryClient.invalidateQueries(['getNewOrders']); // not work
},
},
);
};
second way
const {mutateAsync: onAcceptOrder, isLoading} = useMutateAcceptedOrder();
const acceptOrder = async (orderId: string) => {
const body = {
device: 'iPhone',
version: '1.0.0',
location_lat: '10.10',
location_lng: '10.10',
orderId: orderId,
os: Platform.OS,
source: 'mobile',
token: userInfo.token,
};
await onAcceptOrder(body);
queryClient.refetchQueries(['getNewOrders']); // not work
queryClient.invalidateQueries(['getActiveOrders']); // not work
handleClosetModalPress();
};
sample of query I wanted to refetch after the success
export const useNewOrders = (bodyQuery: {token: string | null}) => {
console.log('token>>', bodyQuery.token);
return useQuery(['getNewOrders'], () => getNewOrders(bodyQuery),
{
enabled: bodyQuery.token != null,
});
};
App.tsx
const App: React.FC<AppProps> = ({}) => {
const queryClient = new QueryClient();
if (__DEV__) {
import('react-query-native-devtools').then(({addPlugin}) => {
console.log('addPlugin');
addPlugin({queryClient});
});
}
useEffect(() => {
RNBootSplash.hide({fade: true}); // fade
}, []);
return (
<GestureHandlerRootView style={{flex: 1}}>
<QueryClientProvider client={queryClient}>
<BottomSheetModalProvider>
<AppContainer />
</BottomSheetModalProvider>
</QueryClientProvider>
</GestureHandlerRootView>
);
};
export default App;
--
EDIT
So after using the react-query-native-devtools Debug tool, I can't see any query in the first tab recorded in the debugger! Although the data fetched well.
So I guess that's why the refetch did not work in this case!
Any query in the first tab I can't refetch it again
Steps to reproduce:
open App - Active Tab (first tab)
check the status of the queries
nothing recorded in the debugger
Navigate to any other screen/tab
Check the status of queries
all screen queries recorded in the debugger
Per https://tkdodo.eu/blog/react-query-fa-qs#2-the-queryclient-is-not-stable:
If you move the client creation into the App component, and your component re-renders for some other reason (e.g. a route change), your cache will be thrown away:
Need to init queryClient like that:
const [queryClient] = React.useState(() => new QueryClient())
I was facing the same issue and haven't been able to find a proper solution for this, however I have found a hacky workaround. You just call your refetch function or invalidate queries inside a setTimeout.
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 use useEffect to rerender postList (to make it render without the deleted post) when postsCount change, but I can't get it right. I tried to wrap everything inside useEffect but I couldn't execute addEventListener("click", handlePost) because I am using useEffect to wait for this component to mount first, before attaching the evenListener.
Parent component:
function Tabs() {
const [posts, setPosts] = useState([]);
const dispatch = useDispatch();
const postsCount = useSelector((state) => state.posts.count);
useEffect(() => {
document.getElementById("postsTab").addEventListener("click", handlePost);
}, [handlePost]);
const handlePost = async (e) => {
const { data: { getPosts: postData }} = await refetchPosts();
setPosts(postData);
dispatch(postActions.getPostsReducer(postData));
};
const { data: FetchedPostsData, refetch: refetchPosts } = useQuery( FETCH_POSTS_QUERY, { manual: true });
const [postList, setPostsList] = useState({});
useEffect(() => {
setPostsList(
<Tab.Pane>
<Grid>
<Grid.Column>Title</Grid.Column>
{posts.map((post) => (
<AdminPostsList key={post.id} postId={post.id} />
))}
</Grid>
</Tab.Pane>
);
console.log("changed"); //it prints "changed" everytime postCount changes (or everytime I click delete), but the component doesn't remount
}, [postsCount]);
const panes = [
{ menuItem: { name: "Posts", id: "postsTab", key: "posts" }, render: () => postList }
];
return (<Tab panes={panes} />);
}
child/AdminPostsList component:
function AdminPostsList(props) {
const { postId } = props;
const [deletePost] = useMutation(DELETE_POST_MUTATION, {variables: { postId } });
const dispatch = useDispatch();
const deletePostHandler = async () => {
dispatch(postActions.deletePost(postId));
await deletePost();
};
return (
<>
<Button icon="delete" onClick={deletePostHandler}></Button>
</>
);
}
The Reducers
const PostSlice = createSlice({
name: "storePosts",
initialState: {
content: [],
count: 0,
},
reducers: {
getPostsReducer: (state, action) => {
state.content = action.payload;
state.count = action.payload.length
},
deletePost: (state, action) => {
const id = action.payload
state.content = current(state).content.filter((post) => (post.id !== id))
state.count--
}
},
});
Okay, let discuss this in separate comment. Key point is to decouple posts logic from wrapper component(Tabs). You should create component dedicated only to posts and render it in wrapper. Like that you can easily isolate all posts-related logic in posts-related component, for example to avoid attaching some listeners from wrapper(because it is not intuitive what you are doing and who listens for what because button is not in that same component). In separated component you will have only one useEffect, to fetch posts, and you will have one selector(to select posts from redux), and then just use that selection to output content from component.
That part <Tab panes={...} /> was the source of most of your problems, because like that you are forced to solve everything above <Tab../> and then just to pass it, which is not best practice in you case since it can be too complicated(especially in case when you could have multiple tabs). That is why you need to decouple and to create tab-specific components.
This would be an idea of how you should refactor it:
function PostsTab() {
const posts = useSelector((state) => state.posts?.content ?? []);
useEffect(() => {
// Here dispatch action to load your posts
// With this approach, when you have separated component for PostsTab no need to attach some weird event listeners, you can do everything here in effect
// This should be triggered only once
// You can maybe introduce 'loading' flag in your reducer so you can display some loaders for better UX
}, []);
return (
<div>
{/* Here use Tab components in order to create desired tab */}
<Tab.Pane>
<Grid>
<Grid.Column>Title</Grid.Column>
{posts.map((post) => (
<AdminPostsList key={post.id} postId={post.id} />
))}
</Grid>
</Tab.Pane>
</div>
);
}
function Tabs() {
return (
<div>
<PostsTab/>
{/** HERE you can add more tabs when you need to
* Point is to create separate component per tab so you can isolate and maintain tab state in dedicated component
and to avoid writing all logic here in wrapper component
* As you can see there is no need to attach any weird listener, everything related to posts is moved to PostsTab component
*/}
</div>
);
}
Ok, let's discuss what I did wrong for the future reader:
There is no need to use this weird spaghetti
useEffect(() => {
document.getElementById("postsTab").addEventListener("click", handlePost);
}, [handlePost]);
const panes = [
{ menuItem: { name: "Posts", id: "postsTab", key: "posts" }, render: () => postList }
];
for I could've used a <Menu.Item onClick={handleClick}>Posts</Menu.Item> to attach the onClick directly.
I had to use useEffect to monitor posts dependency, but .map() will automatically update its content if the array I am mapping had any changes so there is no need to use it use useEffect in this context.
I think I can use lifting state to setPosts from the child component and the change will trigger .map() to remap and pop the deleted element, but I couldn't find a way to so, so I am using a combination of redux (to store the posts) and useEffect to dispatch the posts to the store than I am mapping over the stored redux element, idk if this is the best approach but this is all I managed to do.
The most important thing I didn't notice when I almost tried everything is, I must update apollo-cache when adding/deleting a post, by using proxy.readQuery
this is how I did it
const [posts, setPosts] = useState([]);
const handlePosts = async () => {
const { data: { getPosts: postData } } = await refetchPosts();
setPosts(postData);
};
const handlePosts = async () => {
const { data } = await refetchPosts();
setPosts(data.getPosts);
};
// Using useEffect temporarily to make it work.
// Will replace it with an lifting state when refactoring later.
useEffect(() => {
posts && dispatch(postsActions.PostsReducer(posts))
}, [posts]);
const [deletePost] = useMutation(DELETE_POST_MUTATION, {
update(proxy) {
let data = proxy.readQuery({
query: FETCH_POSTS_QUERY,
});
// Reconstructing data, filtering the deleted post
data = { getPosts: data.getPosts.filter((post) => post.id !== postId) };
// Rewriting apollo-cache
proxy.writeQuery({ query: FETCH_POSTS_QUERY, data });
},
onError(err) {
console.log(err);
},
variables: { postId },
});
const deletePostHandler = async () => {
deletePost();
dispatch(postsActions.deletePost(postId))
};
Thanks to #Anuj Panwar #Milos Pavlovic for helping out, kudos to #Cptkrush for bringing the store idea into my attention
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
I have a parent component with two buttons. Button 1 should open iFrame with url 1 and button 2 url 2. It seems the way I built it the iFrame is not rebuilt entirely.
parent.js
if (currentTable.id === "Bo3Ko3K") {
<DailyCo url="https://meeting.daily.co/123456" />
} else if (currentTable.id === "9RGmWxX") {
<DailyCo url="https://meeting.daily.co/abcdef" />
}
child.js
import { makeStyles } from "#material-ui/core/styles";
import React, { useRef, useEffect } from "react";
import DailyIframe from "#daily-co/daily-js";
const useStyles = makeStyles((theme) => ({
root: {
width: "100%",
height: "100vh",
border: "0",
},
}));
const DailyCo = ({ url }) => {
const classes = useStyles();
const iframeRef = useRef();
useEffect(() => {
if (!url) {
console.error("please set an url!");
return;
}
const daily = DailyIframe.wrap(iframeRef.current, {
// showLeaveButton: true,
});
daily.join({ url });
}, [url]);
return (
<iframe
className={classes.root}
title="video call iframe"
ref={iframeRef}
allow="camera; microphone; fullscreen"
></iframe>
);
};
export default DailyCo;
You have 2 options I can see to get this working:
You need to leave the meeting before joining a new one. Unfortunately this method is complicated by daily taking about 7ms (in an async manner) to leave a meeting. So, it isn't possible to just have the useEffect clean up normally. It's also fun because daily.leave() returns a promise that never resolves if you aren't in a meeting. Their documentation for leave() is currently misleading:
Leaves the meeting. If there is no meeting, this method does nothing.
Returns null;
https://codesandbox.io/s/reverent-grass-qpjke?file=/src/App.js
const iframeRef = useRef();
const dailyRef = useRef();
const joinedRef = useRef();
useEffect(() => {
dailyRef.current = DailyIframe.wrap(iframeRef.current, {
// showLeaveButton: true,
});
dailyRef.current.on('left-meeting',()=>{
joinedRef.current=false;
})
dailyRef.current.on('joining-meeting',()=>{
joinedRef.current=true
})
console.log("mounted");
return () => {
dailyRef.current.destroy();
console.log("unmount");
};
}, []);
useEffect(() => {
(async () => {
if (joinedRef.current) {
// This is needed due to it never returning
// if there wasn't a meeting joined first...
await dailyRef.current.leave();
}
if (!url) {
console.error("please set an url!");
return;
}
await dailyRef.current.join({ url });
})();
}, [url]);
You need to call daily.destroy() before creating a new Wrap, otherwise they interfere with each other and issues abound and the new one never fully sets up. This issue is described in their documentation for destroy
You can re-use the daily-js call iframe or call object multiple times
(a sequence of join(), leave(), join(), etc method calls will work
fine).
But when you are finished with the daily-js call object, you should
call destroy() to free all resources associated with it.
This is particularly important if you plan to create another daily-js
object in the future. If you don't call destroy(), and later create a
new daily-js call object, the event listeners from the old object and
the new object will interfere with one another.
https://codesandbox.io/s/spring-water-nv3k3?file=/src/App.js
const iframeRef = useRef(null);
useEffect(() => {
if (!url) {
console.error("please set an url!");
return;
}
const daily = DailyIframe.wrap(iframeRef.current, {
// showLeaveButton: true,
});
daily.join({ url });
return () => {
daily.destroy();
};
}, [url]);
In general, I'd recommend to use option 1 if only because it's more performant (less set up and time required to change meetings). Even though it's easier to just destroy the wrap and recreate it.
In my React-App (create-react-app) I use a selector (created with reselect) to compute derived data from stored data.The Problem is, the selector takes a long time to compute. I would like to show a spinner (or a message) on the user interface. But each time the selector is recomputed the ui freezes.
I read a lot of stuff (Web Worker, requestIdleCallback, requestAnimationFrame) and try to make my own React hook but I am stuck. I cannot use the selector in callbacks.
The searched result is simply to get the ui refreshed before the selector is recomputed.
That's my solution. I don't know if it's "good" or if it breaks some rules of react or reselect, but it works. Maybe you can assess that?The code is simplified to improve readability.
The idea is, the selector returns a Promise and I call requestAnimationFrame before computing the data to get a chance to refresh the ui.
selector.js:
const dataSelector = state => state.data;
export const getDataComputedPromise = createSelector([dataSelector], (data) => {
const compute = function () {
return new Promise((resolve) => {
// heavy computing stuff
resolve(computedData);
});
};
return new Promise((resolve) => {
let start = null;
let requestId = null;
function step (timestamp) {
if (!start) {
start = timestamp;
window.cancelAnimationFrame(requestId);
requestId = window.requestAnimationFrame(step);
return;
};
compute().then(freeResources => {
window.cancelAnimationFrame(requestId);
resolve(freeResources);
});
}
requestId = window.requestAnimationFrame(step);
});
});
myPage.js
const MyPage = ({ someProps }) => {
...
const dataComputedPromise = useSelector(getDataComputedPromise);
const [dataComputed, setDataComputed] = useState([]);
const [computeSelector, setComputeSelector] = useState(false);
useEffect(() => {
setComputeSelector(true);
}, [data]);
useEffect(() => {
dataComputedPromise.then(dataComputed => {
setDataComputed(dataComputed);
setComputeSelector(false);
});
}, [dataComputedPromise]);
...
return <div>
<Loader active={compueSelector}>Computing data...</Loader>
</div>;
};
export default MyPage;