How to re-fetch single items of a collection in react-query - reactjs

Let's say i have a query to fetch a collection of movies:
useQuery(['movies'], getMovies)
Now, if I want to re-fetch only one movie instead of all I can write something like this:
useQuery(['movies', movieId], () => getMovie(movieId))
Problem is that I use a different query key and that it will duplicate the data. I'll have the movie in my cache twice.
So, what's the react-query way of updating a single item of a fetched collection? All components that use useQuery(['movies']) should be updated automatically when the single item was fetched.

I think there are two ways:
like you described with a new key. yes, the data will be "twice" in the cache, but oftentimes, the "list" data has a different structure than the "detail" data. movies-list might just have id and name, while the details have a description etc. as well. If you do that, make sure to give them both the same prefix ('movies'), so that you can utilize the fuzzy matching when invalidating: queryClient.invalidateQueries(['movies']) will then invalidate the list and all details, which is nice.
you can use the select option to build a detail query on:
const useMovies = (select) => useQuery(['movies'], getMovies, { select })
const useMovie = (id) => useMovies(movies => movies.find(movie => movie.id === id))
with that, you can do useMovie(5) and thus will only subscribe to the movie with id 5. This is really nice and also render optimized - if the movie with id 4 updates, the component that subscribes to movie with id 5 will not re-render.
the "drawback" is of course that for the data / network perspective, there is only one query - the list query, so everytime you do background refetches, the whole list will be queried from the backend. But it's still a nice approach to avoid data duplication in the cache, and it makes optimistic updates easier as well, because you only have to write to update one cache entry.

Related

Create a reusable complex table component

I am mainly a backend developer trying to do some React and I have the following question for you:
The website I’m working on has a lot of pages that are very similar between them. Most of them have one or two tables that also have pagination, filtering, sorting on some rows and row selection (there are some tables that do not have pagination or filtering or sorting or row selection). Parameters coming from the url should be allowed and applied to the filters (and thus to the first data fetch), selected rows have batch actions and the button shows at the top of the table, each row has buttons in the last column to do actions like edit, delete, etc. Also one final think that complicates all this a bit more is that some tables have cells that have more than just text, they can have icons to the left, rignt or have 2 lines of text.
The tables look something like this:
I created hooks to handle the query params, filters and pagination, which I thought was going to be enough but I still have a lot of duplication. In my components I have code similar to this:
const [dataLoading, setDataLoading] = useState(false);
const [data, setData] = useState([]);
const {
queryParams, clearQueryParams, setQueryParams,
} = useQuery();
const {
filters, updateFilters, clearFilters,
} = useFilters(queryParams);
const {
paging, setPaging, setPage, setRowsPerPage,
} = usePagination();
// A lot of funtions that use state (less than before though)
I thought of creating a Table component that contains all of this logic and receives the functions it needs via props, but there is also a problem which is that there are certain actions outside the table that can make the data dirty and a reload is required (like a new element is present on the list or an item was deleted for example).
What would be a good approach to solve this while making the code reusable (easy to read and simple is a huge plus)?
Do you think having one complex Table component is a good idea or is it better to have a PaginatedTable, FilterableTable (or maybe there is a way to nicely compose those)?
Thanks

What's the most efficient and scalable way to handle filtering and sorting of a large collection in cloud firestore?

I have a large collection where each document is of a substantial size and therefore cannot be embedded into a single document. For my feature, I need to sort the collection using orderBy and filter the collection using "array-contains" and "==" where the sort and filter parameters are provided by the user. I was wondering if there was an efficient way to do this by cacheing documents that had already been fetched in previous queries. That being said, does firebase do any cacheing itself and does it already optimize what I'm trying to do in this case, or is there any custom cacheing/optimization I can do?
This is what my implementation looks like right now. This works fine for now however it's not very scalable as it creates a new realtime listener each time any of the filter/sort state changes. Is there any way can I improve this to minimize the total number of documents read?
useEffect(() => {
if (!sort || !status || !search) return;
const unsubscribe = firebase.collection("users")
.where("searchTerms", "array-contains", search)
.where("status", "==", status)
.orderBy(sort)
.onSnapshot((snapshot) => {
// update state hook with snapshot data...
});
return () => unsubscribe();
}, [sort, status, search]);
Thank you for any and all help!
Typically, you will want to use a search indexer to enable functionality like this.
The Firebase documentation recommends using Algolia for full-text search. You want to create a cloud function that indexes the data you want to search on, along with the Firestore document ID. On the frontend, you can use the Algolia API to get search results and then fetch the whole document From Firestore when you need to display it.
An alternative to pagination for "big data" and still supporting complex queries can be done by baking the data for the client into a simplified search collection.
This displays only key information that represents a source document by combining the essential data into a dedicated collection of all the results.
Each document can hold up to 1MB of data each and that can equate to roughly 10k-200k entries based on your data size. It does take some time to set up but it has been effective for handling long-lived data within firebase without additional 3rd party solutions.
The key takeaways are as follows:
This is ideal for data that doesn't update too frequently, multiple changes at once can hit the 1 second limit per document.
All search documents contain two properties, a counter to maintain the current entries and an array of strings that represent your essential data.
Each source document needs to maintain a document ID of its entry document for future updates
On update, you find the search index ID, and use arrayUnion and arrayRemove methods, preferably with a transaction and update the source document.
Optionally, you can use the new Bundle Method to bundle this collection with your app
Resources:
https://firebase.google.com/docs/functions/firestore-events#event_triggers
https://firebase.google.com/docs/firestore/manage-data/add-data#update_elements_in_an_array
https://firebase.googleblog.com/2021/04/firestore-supports-data-bundles.html

Can you update firestore query dynamically based on router location?

So basically, I have a simple React app connected to Firebase that lists different types of food from firestore collections.
Example:
I have a few categories. The default one is "All" that displays top 8 popular dishes from all other available categories and this part is easy but I want an user to be able to click on other category and update my query.
Category is actually a NavLink that updates location on click so: if user click on "Pizza" category the url looks like this localhost:3000/Pizza if he clicks on Salad it is localhost:3000/Salad etc.
I have a "Wall" component that is a section and it displays those items from firestore.
My query ref in this wall component look like this: const foodRef = db.collection("food").doc("all").collection("items");
But I want to set .doc dynamically and make query on every update so I changed the query to something like that:
const location = useLocation();
const foodRef = db.collection("food").doc('${location.pathname}').collection("items");
And when user click on different Card (NavLink) url updates but query does not.
I know it is a bad solution but I actually have no idea how to do that.
I have read about Recursive Paths in react router but I do not know if it is what I am looking for.
If you know how to approach that please let me know.
Thanks for your time.
Firestore does not support wildcards or replacements in queries and Query objects are fully immutable (they can't be changed). You have to know the names of the documents and collections ahead of time to build a query. If you want to change some part of a query, you have to rebuild a whole new query object every time, and run the query again to get a new set results.

How can I access sub collection within every document in collection?

I want an user to be able to select different types of food that will be displayed on wall (only 8 items from collection) by changing category in my app.
I have two ideas how to do that but I do not know if they are good.
First is to get items from firestore only on selected category. Example:
const foodRef = db.collection("food").doc('${category}').collection('items');
and just get those items and then set state with those items.
This is how I do it by now and it is working good but I think it is not good because every time user change the category components must re-render and user is making unnecessary queries to firestore.
So I thought that it would be good to get all "items" from every document and store it in an object and then operate on that object based on selected category.
My problem is that I tried to get sub-collection of every document in collection but I actually do not know how to do that.
I tried something like that:
const getItems = async () => {
let snapshot = await firebase.firestore().collection('food').get();
return snapshot.docs.map(doc => doc.collection('items').forEach(item => item.data()))
}
I know it will not gonna work but I wanted to do something like that.
Structure in my firestore looks like that:
food => [16 documents] => items => [8 documents]
So how should I do that guys, get all the items at once or render every time user change category?
Thanks for your time and help in advance guys:)
You can use firestore collectionGroup to get multiple subcollections in different documents. Take a look at this link for details about collection groups.
https://firebase.googleblog.com/2019/06/understanding-collection-group-queries.html
In your case, you could probably do something like this:
const everyItems = await firebase.firestore().collectionGroup('items').get();
Let me know if you want more details :)

How to handle cache update after mutation in multiple views?

This is more of a open question but hopefully it won’t get deleted.
I am using react and apollo although the question is more general.
Let’s say I have 3 distinct views in my app all using similar (but not the same) data.
All of them are using separate queries but each of the query uses common operation but with slightly different data returned.
Let’s say I have a mutation somewhere that adds something to data (think of a list of items and a new item being added).
Let’s say after mutation I want to update cache to reflect that change. I am using read/writeQuery to do the update.
With this setup I need to update 3 queries - this becomes a maintenance nightmare.
After some reading I figured I am doing this wrong - I have now created a single query - now I need to only update that single query after mutation and all of my views are updated automatically.
However the problem is that this query now has to download all the data that all 3 views combined need - feels like this is very inefficient, because some of the views will get data they'll never use.
Is there a better way to do it?
Please note that read/writeFragment won't work because they won't update the underlying queries - check this answer for example: https://stackoverflow.com/a/50349323/2874705
Please let me know in comment if you need a more concrete example.
All in all I think in this setup I would just be better with a global state handling and avoid apollo cache all together - however I feel cheated cause apollo promised to solve the state problems :)
EDIT
Here's a concerete example:
Let's say our graphql schema is defined liked this:
type Post {
id: ID!
title: String!
body: String
published: Boolean!
}
type Query {
posts(published: Boolean): [Post!]!
}
type Mutation {
createDraft(body: String!, title: String!): Post
publish(id: Int!): Post
}
Now, we create 3 queries and 2 mutations on the client
query PostTitles {
posts {
id
title
}
}
query Posts {
posts {
id
title
body
published
}
}
query PublishedPosts {
posts (published: true) {
id
title
body
published
}
}
mutation CreateDraftPost ($body: String!, $title: String!) {
createDraft(body: $body, title: $title) {
id
title
body
published
}
}
mutation PublishPost ($id:ID!) {
publish (id: $id) {
id
published
}
}
Just to note createDraft creates a post with the default false published value.
How can use either of those mutations to create or publish a post and have all the 3 cached queries to be updated without using refetchQueries or manualy updating each of the query?
I think the real problem is that each of those queries are stored separately in the apollo in-memory cache.
From my experience, here's how it should goes.
In the case of CreateDraftPost mutation:
You call the mutation and also pass an update function. In this update function, you modify the cache of the root query posts by creating a new fragment of Post and then add this fragement into posts. See this: https://www.apollographql.com/docs/react/data/mutations/#making-all-other-cache-updates
Since the PostTitles and Posts all rely on the root query posts (just differ in the queried fields) and the new fragment of Post you've just added into posts has sufficient fields, your PostTitles and Posts should automatically reflect the changes.
Since CreateDraftPost always create a draft with published defaults to false. You don't need to update anything related to PublishedPosts query.
In the case of PublishPost mutation:
You call the mutation and the returned result is a Post with updated fields (id, published). By the mechanism of Apollo GraphQL cache, this Post (identified by id) will be updated in any queries it has involved. See this: https://www.apollographql.com/docs/react/data/mutations/#updating-a-single-existing-entity
However, you need to manually update the PublishedPost query. Do this by providing update function in the mutation call. In this update function, you will readQuery of PublishedPost first, create a new Post out of the returned data and finally writeQuery to add this post into the PublishedPost results. Reference this: https://www.apollographql.com/docs/react/caching/cache-interaction/#combining-reads-and-writes
How about using refetchQueries:
In the case of CreateDraftPost mutation, refetch only Posts query should be sufficient (the PostTitles should be updated accordingly) since both Posts and PostTitles rely on the same root query posts and fields in Posts has also covered fields in PostTitles
In the case of PublishPost mutation, I would prefer refetch the PublishedPost query to avoid doing the whole update thing (since I'm lazy and I think it will not cost me much to refetch 1 query)
It sounds like you've looked into and used the update argument that can be passed to mutation functions returned from useMutation. You're probably using proxy.readQuery and proxy.writeQuery to update it (or letting this magic happen in the background). If not, here is the documentation.
Another approach that is similar in concept but finer detail is to use proxy.readFragment and proxy.writeFragment. You can specify a set of properties on a type as being part of a fragment, and update that fragment whenever new data comes in. The nice part is that this fragment can be used within any number of queries, and if you update the fragment, those queries will update.
fragment documentations

Resources