Need help on using mutation with useSWRInfinite
EXPECTED
On my posts page, I used useSWRInfinite to do load posts with pagination. it provides me a bounded mutation which I can use on that page
The user is then able to select the post and go to a postDetails page where they can see details of the post and delete it
On the postDetails page, I want to allow the user to delete a post which will open a modal asking him to confirm delete. Once the user clicks on confirm delete, the post will be deleted via an axios.delete call and then the posts will be mutated and user will be sent back to the posts page. the new paginated data will no longer show the deleted post
PROBLEM
The delete function is where the problem lies. When i use my usePagination hook to get the bounded mutate function which I call after deleting the data, I get a key conflict error. I also tried doing mutate with the key to my api but it doesn't update the cache.
QUESTION
Is there anyway I can use the bounded mutation across nextjs pages? or is there something else I should do to make my delete function works? Thanks
// todo
const onDelete = async (postId: string) => {
await axios.delete(`/api/demo/posts/${postId}`);
//mutate(`/api/demo/posts`); - doesn't mutate the data
mutatePosts(); // mutates the data but gets conflicting keys
replace("/demo/posts");
};
The mutate from calling the usePagination function will lead to key conflict error
const { mutate: mutatePosts } = usePagination<IPost>(`/api/demo/posts`);
Using the mutate from useSWRConfig() and passing in the api key '/api/demo/posts' will not update the cache at all.
const { mutate } = useSWRConfig();
I used usePagination which is a custom hook to make the api call in posts page. However, I am unable to use the bounded mutate here across nextjs pages in the postDetail page
const {
paginatedData: paginatedPosts,
error,
isLoadingMore,
isReachedEnd,
setPage,
page,
mutate: mutatePosts,
} = usePagination<IPost>(`/api/demo/posts`, searchText);
usePagination custom hook
import useSWRInfinite from "swr/infinite";
export const usePagination = <T>(url: string, searchText: string = "") => {
const PAGE_SIZE = 2;
const getKey = (pageIndex: number, previousPageData: T[]) => {
pageIndex = pageIndex + 1;
if (previousPageData && !previousPageData.length) return null; // reached the end
return `${url}?page=${pageIndex}&limit=${PAGE_SIZE}&searchText=${searchText}`;
};
const {
data,
size: page,
setSize: setPage,
error,
isValidating,
mutate,
} = useSWRInfinite(getKey);
const paginatedData: T[] = [].concat.apply([], data!);
const isLoadingMore = data && typeof data[page - 1] === "undefined";
const isReachedEnd = data && data[data.length - 1]?.length < PAGE_SIZE;
return {
paginatedData,
isLoadingMore,
isReachedEnd,
page,
setPage,
isValidating,
error,
mutate,
};
};
Related
i have a project that's using Nextjs and Supabase. I was using context API and now i'm trying to replace it for React Query, but i'm having a hard time doing it. First of all, can i replace context completely by React Query?
I created this hook to get the current user
export const getUser = async (): Promise<Profile> => {
const onFetch = await supabase.auth.getUser();
const userId = onFetch.data.user?.id;
let { data, error } = await supabase
.from("profiles")
.select()
.eq("id", userId)
.single();
return data;
};
export const useUser = () => {
return useQuery(["user"], () => getUser());
};
I'm not sure how to trigger it. When i was using context i was getting the user as this. If data, then it would redirect to the HomePage
let { data, error, status } = await supabase
.from("profiles")
.select()
.eq("id", id)
.single();
if (data) {
setUser(data);
return true;
}
Since i was getting the user before redirecting to any page, when i navigated to profile page, the user was already defined. How can i get the user before anything and keep this state? I suppose that once the user is already defined, at the profile component i can call useUser and just use it's data. But it's giving me undefined when i navigate to profile, i suppose that it's fetching again.
const { data, isLoading } = useUser();
But it's giving me undefined when i navigate to profile, i suppose that it's fetching again.
Once data is fetched when you call useUser, it will not be removed anymore (unless it can be garbage collected after it has been unused for some time). So if you do a client side navigation (that is not a full page reload) to another route, and you call useUser there again, you should get data back immediately, potentially with a background refetch, depending on your staleTime setting).
If you're still getting undefined, one likely error is that you are creating your QueryClient inside your app and it thus gets re-created, throwing the previous cache away. You're not showing how you do that so it's hard to say. Maybe have a look at these FAQs: https://tkdodo.eu/blog/react-query-fa-qs#2-the-queryclient-is-not-stable
I was asked a question regarding SWRs "loading" state:
How do you create a loading state from SWR between different URL fetches?
Their docs make it appear straight forward:
const { data, error } = useSWR(`/api/user/${id}`, fetcher)
const isLoading = !error && !data;
However, this logic seems to fail after the first render of the hook/component. On the first render data is undefined. Then loads and data becomes a value to consume in the UI.
Let's say I change the id via the UI and want to show loading indicator. Because data is no longer undefined, the same logic fails.
There is an additional item returned isValidating. So I updated my logic:
const isLoading = (!data && !error) || isValidating
However, this could be true when:
there's a request or revalidation loading.
So in theory something else causes my component to rerender. This could inadvertently cause a "revalidation" and trigger loading state gets shown. This could break the UI temporarily, by accident.
So how do you derive "loading" between URL changes without revalidation? I am trying to replicate how graphQL Apollo Client returns const { loading, error, data } = useQuery(GET_DOGS);
Let's say I change the id via the UI and want to show loading indicator. Because data is no longer undefined, the same logic fails.
data will be undefined again when you change the key (id), if it doesn't have a cache value.
Remember that in SWR { data } = useSWR(key) is mentally equivalent to v = getCache(k), where fetcher (validation) just write to the cache and trigger a re-render.
data is default to undefined, and isValidating means if there's an ongoing request.
Alternatively, you can derive loading through the use of middleware. Here's what I use...
loadingMiddleware.ts
import { useState } from 'react'
import { Middleware } from 'swr'
const loadingMiddleware: Middleware = (useSWRNext) => (key, fetcher, config) => {
const [loading, setLoading] = useState(false)
const extendedFetcher = (...args) => {
setLoading(true)
try {
return fetcher(...args)
} finally {
setLoading(false)
}
}
const swr = useSWRNext(key, extendedFetcher, config)
return { ...swr, loading }
}
export default loadingMiddleware
App.tsx
import { SWRConfig } from 'swr'
import loadingMiddleware from './loadingMiddleware'
const App: FC = () => {
...
return (
<SWRConfig value={{ use: [loadingMiddleware] }}>
...
</SWRConfig>
)
}
export default App
Update (12/13/22)
swr#v2 is out and provides isLoading and isValidating properties in the return value of useSWR.
Here's the difference between the two according to the swr docs.
isValidating becomes true whenever there is an ongoing request whether the data is loaded or not.
isLoading becomes true when there is an ongoing request and data is not loaded yet.
I am learning react, and given this simple example of using SWR to fetch some items from an API and showing the items with groups using fluentui DetailedList - I am running into a problem with the groups.
Whenever I click a group in UI to collapse/uncollapse, that seems to trigger a rerender, and then the component will createGroups(data) again which resets the UI again back to original state as the groups object is recalculated.
Where am I supposed to actually store / calculate the groups information of my data? Initial it needs to be created, but from there it seems that it should only needs to be reevaluated whenvere the swr api returns new data - and then i still properly would want to merge in the current state from collapsed groups that the user might have changed in the UI.
Is it because i properly should not use SWR as it refreshes data live - and only do it on page refresh?
const SWR = ({ children, listid, onSuccess }: { children: ((args: SWRResponse<any, any>) => any), listid: string, onSuccess?: any }) => {
const url = `http://localhost:7071/api/Lists/${listid}`;
console.log(url);
const {data,error } = useSWR(url, { fetcher: fetcher, isPaused: () => listid === undefined, onSuccess });
const items = data.value;
const groups = createGroups(data)
return <... DetailsList group={groups} items={items} ... >; // ... left out a few details ...
};
What about adding a state for holding the groups and an useEffect for when data changes and insde the useEffect you should check if the content has changed before updating the groupState.
const hasChanged(data) => {
return data.notEquals(state.data)); // write your own logic for comparing the result
};
useEffect(() => { if (hasChanged(data)) {
setState(prev=> ({ ...prev, group: createGroup(data), data: data });
}}, [data]);
You dont actually need to store the group, you can just hold the data in your state, but the important part is to be able to check if any change actually took place before changing the state.
Another thing worth trying is the compare option in the useSWR hook. So instead of placing the "hasChanged" logic inside an useEffect hook, perhaps it could be in the compare function. Haven't had the chanse to test this myself though.
A third and final option would be to place the creation of groups inside your fetcher. Perhaps the most intuitive solution for this particular case, though I'm not completely sure it will prevent the unnecessary re-renders.
const fetcher = url => axios.get(url).then(res=> {
return {
items: res.data.value,
groups: createGroups(res.data),
};
});
const SWR = ({ children, listid, onSuccess }: { children: ((args: SWRResponse<any, any>) => any), listid: string, onSuccess?: any }) => {
const { data, error } = useSWR(url, fetcher, ...);
return <... DetailsList group={data.groups} items={data.items} ... >; // ... left out a few details ...
};
I'm just playing around with react query
I'm building sort of a simple github clone
I have to use useQuery twice one for the current user
as router param the other with a new search.
I ended up with:
const history = useHistory();
const currentUser: string = useRouterPathname();
const [user, setUser] = useState(currentUser);
const handleFormSubmit = (data: SearchFormInputs) => {
history.push(`/${data.search}`);
setUser(data.search);
};
const { isLoading, error, data } = useGetUserData(user);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>An error has occurred: " + {error}</p>;
console.log(user, data);
Is it the right way?
The important part is probably that in useGetUserData, you put the user into the queryKey of react-query, something like:
const useGetUserData = (user) => useQuery(['users', user], () => fetchUserData(user))
so that you always refetch data when the user changes and users are not sharing a cache entry between them.
Something not react-query related though: The good thing about react-router is that you can also use it as a state manager - their hooks also subscribe you to changes, so there is no real need to duplicate that with local state:
const history = useHistory();
const currentUser: string = useRouterPathname();
const handleFormSubmit = (data: SearchFormInputs) => {
history.push(`/${data.search}`);
};
const { isLoading, error, data } = useGetUserData(currentUser);
once you push to the history, all subscribers (via useParams or useLocation) will also re-render, thus giving you a new currentUser.
Lastly, I would recommend checking for data availability first rather than for loading/error:
const { error, data } = useGetUserData(user);
if (data) return renderTheData
if (error) return <p>An error has occurred: " + {error}</p>;
return <p>Loading...</p>
it obviously depends on your use-case, but generally, if a background refetch happens, and it errors, we still have data (albeit stale data) that we can show to the user instead. It's mostly unexpected if you see data on the screen, refocus your browser tab (react-query will try to update the data in the background then per default), and then the data disappears and the error is shown. It might be relevant in some cases, but most people are not aware that you can have data and error at the same time, and you have to prioritise for one or the other.
I am relatively new to react, react native and redux and I have one question about fetching the data from the api and updating the state.
What I want to do is:
- each time a user opens the screen (i.e Posts) I want to fetch data from the api and update the state.
So that:
- I always have newly fetched data in the state and rendered on the screen.
If I fetch the data only in componentDidMount lifecycle method then I won't always have fresh data.
Example:
There are two screens Post and Posts. If a user opens posts screen for the first time then componentDidMount is triggered and posts are fetched and rendered. Then he goes to Post screen adds a new post and goes back to Posts screen then he will not see that new post because the posts will be displayed from the state fetched in componentDidMount method.
What is the best way to handle this and always have latest data?
class Posts extends Component {
componentDidMount() {
this.props.fetchPosts();
}
componentDidUpdate(prevProps) {
this.props.fetchPosts();
}
render () { ... }
}
const mapStateToProps = state => {
return {
posts: state.posts.posts,
loading: state.posts.loading,
}
}
const mapDispatchToProps = dispatch => {
return {
fetchPosts: () => dispatch(actions.fetchPosts()),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Posts)
Action
export const fetchPosts = () => {
return dispatch => {
dispatch(fetchStarted());
apiFetchPosts()
.then(({data}) => {
const posts = data.data;
dispatch(fetched(posts))
})
.catch(error => {
dispatch(fetchFailed(error));
})
}
}
const fetchStarted = () => {
return {
type: actionTypes.POSTS_FETCH_START,
}
}
const fetched = (posts) => {
return {
type: actionTypes.POSTS_FETCH_SUCCESS,
posts,
}
}
const fetchFailed = (error) => {
return {
type: actionTypes.POSTS_FETCH_FAILED,
}
}
Update as you go
In your case, you should be sending a POST request to insert a new post. That request should have the newly created post as a response. You should use that response to update the list of posts you have in the redux state. Simply add the new post to the list.
The same goes for removing posts, once you send a DELETE request, you should also remove that post from the state.
Add timed cache
If you want to refetch the posts when you haven't fetched it for a while, you should implement some kind of caching solution:
When you fetch the posts for the first time you can also store the timestamp of the request. When the user visits the Posts page, you can check if the timestamp is too old e.g. 5 minutes in the past, and refetch it accordingly.
Refetch everytime
If you want to simply refetch the posts every time the user visits that page, you can use the useEffect hook or a combination of componentDidMount and componentDidUpdate.
Please note:
You may call setState() immediately in componentDidUpdate() but note that it must be wrapped in a condition like in the example above, or you’ll cause an infinite loop.
Source
You could use componentDidUpdate