React not re-rendering items, when array gets replaced - reactjs

We are using Next.js + ApolloClient and we have a simple pagination, where, when I click on "2" the next page should be fetched and displayed.
This looks somewhat like this
export default function Reviews ({ rating }) {
const router = useRouter()
const id = router.query.id
const [page, setPage] = useState(1)
const [comments, setComments] = useState(rating.comments) // This is fetched by the server. Initial data
const [fetchComments] = useLazyQuery(GET_RATING_COMMENTS, {
variables: {
id,
page,
first: 3
},
onCompleted: data => {
setComments(data.comments)
}
})
const handleOnPageChange = page => {
setPage(page)
fetchComments()
}
const reviews = comments.map(rating => (
So, it seems like ApolloClient caches the result, which is cool. So the pagination AND rerender works perfectly fine, when the result gets initially fetched per a HTTP Request (not cached) but after ApolloClient has cached the result and I revisit a page that I've clicked before (for instance 2) then reviews is not getting updated.
Which is weird because, onCompleted gets called with the cached result. So I set it but its not updating anymore. The list does not change.
The list only changes initially, when the Page has not been visited yet. If its cached, nothing happens anymore. Why?
Btw, if I add fetchPolicy: 'no-cache' it magically works.

Related

Laravel Inertia React preserve state not working

I'm trying to reload my page but keep the filters state with Inertia.
Like this:
const [allProducts, setAllProducts] = useState([]);
const [filters, setFilters] = useState([]);
const fetchProducts = (page) => {
Inertia.get(
`/products?page=${page}`,
{ filters },
{
preserveState: true,
preserveScroll: true,
only: ['products'],
onSuccess: (response) => {
setAllProducts(allProducts.concat(response.props.products.data));
page += 1;
}
}
)
});
The problem is that filters and allProducts are reset even though preserveState = true.
What do you mean by "reload my page"? State in React is not preserved on "real" page refresh.
Maybe you can save data in local storage, then load it by useEffect. Another solution is to store data you want to keep between pages in Laravel session storage.

Using SWR Bound Mutation Across Nextjs Pages with useSWRInfinite

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,
};
};

useSWR integration with pagination on backend

When page changes, new query is created and it's data is set to initialData.
In this case user sees initialData before new query data is fetched:
import React from "react";
import fetch from "../lib/fetch";
import useSWR from "swr";
function Component({ initialData }) {
const [page, setPage] = React.useState(1);
const { data } = useSWR(
`/api/data?page=${page}`,
{ initialData }
);
// ...
}
Component.getServerSideProps = async () => {
const data = await fetch('/api/data?page=1');
return { initialData: data };
};
My issue is that initialData is used as a fallback every time I query for new data:
Do you have any ideas on how I can prevent this flicker?
So in react-query, I think there are multiple ways to avoid this:
keepPreviousData: true
this is the main way how to do pagination, also reflected in the docs as well as the examples.
It will make sure that when a query key changes, the data from the previous queryKey is kept on the screen while the new fetch is taking place. The resulting object will have an isPreviousData flag set to true, so that you can e.g. disable the next button or show a little loading spinner next to it while the transition is happening. It is similar in ux what react suspense is going to give us (somewhen).
initialData function
initialData accepts a function as well, and you can return undefined if you don't want initial data to be present. You could tie this to your page being the first page for example:
function Component({ initialData }) {
const [page, setPage] = React.useState(1);
const { data } = useQuery(
['page', id],
() => fetchPage(id),
{ initialData: () => page === 1 ? initialData : undefined }
);
}
keep in mind that you will have a loading spinner then while transitioning, so I think this is worse than approach 1.
since your initialData comes from the server, you can try hydrating the whole queryClient. See the docs on SSR.
initialData works very well with keepPreviousData, so here is a codesandbox fork of the example from the doc where initialData is used (solution 1). I think this is the best take: https://codesandbox.io/s/happy-murdock-tz22o?file=/pages/index.js

Too many re-renders. When setting state

I'm trying to make an e-commerce app using expo with typescript and I'm having issues with loading my categories page.
There's two ways which one can access the category page
Through bottom nav - this one works fine
Through buttons on the home page
Trying to setState based off the params that's coming in through the homepage but it's giving me the Too many re-renders error.
Below is the code that I currently have for it.
The default needs to be there because of the first method of entering the category page, I've tried putting a conditional as the default category, but category wont change the second time if being accessed through the home page again.
export default function CategoryList(props: Props) {
let { params } = useRoute<StackRouteProp<'Home'>>()
const [currentCategory, setCategory] = useState({categories: Categories[0]});
if (params) {
setCategory({categories: params.categories})
}
}
You can also use useEffect with useRef:
export default function CategoryList(props: Props) {
let { params } = useRoute<StackRouteProp<'Home'>>()
const [currentCategory, setCategory] = useState({categories: Categories[0]});
const canSet = useRef(true)
useEffect(() => {
if (params && canSet.current) {
setCategory({ categories: params.categories })
canSet.current = false
} else {
canSet.current = true
}
}, [params])
Everytime you call setCategory, it will re-render the component. Then, every time you render the component, if params exists, it will call setCategory again, which will cause another render, which calls setCategory again, and so on and on until it hits React's render limit.
One possible way to work around this would be to add a boolean to set after the first time which will prevent it from going inside the if block the second time:
const [currentCategory, setCategory] = useState({categories: Categories[0]});
const [hasParamsCategory, setParamsCategory] = useState(false);
if (params && !hasParamsCategory) {
setCategory({categories: params.categories});
setParamsCategory(true);
}
This still isn't perfect because in general it's not a good practice to directly call state-changing functions during render, but this will serve as a quick and dirty workaround.

Issue with refetching specific GraphQL obvervable query

I'm very new to React and GraphQL(Apollo) and learning the stack.
I have a React Component that loads some page data and there is a "Reload" button on the page.
Problem:
I want to refetch the data when I click on "Reload" button.
However, I could not find a way to refetch the data from queryObservable.
How do I refetch the data from QueryObservable? Is there anyway to call refetch?
I tried using reFetchObservableQueries() however, this reloads the page and refetches everything. Can I specifically refetch only a single observable?
This is my code:
getPageQueryObservable = (contentId, key, paginated = false) => {
const query = contentId ? TreeQuery : RootLevelQuery;
const defaultVariables = 'Test';
const variablesForPagination = {
paginationLimit: INITIAL_PAGE_SIZE,
...defaultVariables
};
return this.props.apolloClient.watchQuery({
query,
variables: paginated === true ? variablesForPagination : defaultVariables
});
};
loadPages = async (paginated = false) => {
const { pages: localPages } = this.state;
const { id, key } = this.props;
const queryObservable = this.getPageQueryObservable(
id,
key,
paginated
);
// I want to ideally call this stuff again..When "Reload" button is clicked.
this.querySubscription = queryObservable.subscribe({
next: ({ data, loading }) => {
....
});
You can call query with the same query and variables but a fetchPolicy of network-only in order to refetch the query. This should update the cache and therefore any relevant Observables.
await client.query({ query, variables, fetchPolicy: 'network-only' })
However, there's really no reason to use watchQuery directly. Instead, you should use the hook API to fetch and refetch your data.
const { data, loading, refetch } = useQuery(query, { variables })
Not only does this reduce boilerplate, but now refetching is as simple as
await refetch()

Resources