I have a table that has the data of a different fetch per row. Each row will also be able to trigger a fetch independently of the other rows. If each row is a component that has a button to refetch, the implementation is easy. I just put call useQuery per row (a component).
The problem is filtering and sorting, because the fetched data is only on the rows so there is no global list containing all the information.
I tried to implement it with useQueries or just use components. But I only come up with gnarly solutions. One of those would be to have row components that call useQuery and also set a value (useState) on a parent. This looks like I'm setting the same data at 2 levels and if I get a big table that virtualizes rows, the useQuery inside the components are not triggered because the component is not created.
The problem is hard to describe, so if there is some part that needs clarification please let me know.
===
This is not the real code, just code to try to represent what I have:
function Row({cell}) {
const [fetch, setFetch] = useState(null);
const query = useQuery(["somekey", refetch], fetchFn(cell.url))
const refetch = () =>setFetch(Date.now())
return (<div onClick={refetch}>{query.data.value}</div)
}
function Table({array}) {
return (<div>
{array.map(el => <Row cell={el}/>})}
</div>)
}
This is generally not the easiest to implement with react-query, because it doesn't have a normalized cache, but here is how I would approach the problem:
have one list query: useQuery('myList')
each row has it's own query: useQuery('myList', id)
I would use initialData and staleTime to pre-populate the detail query with data from the list query and avoid unnecessary fetches when each row mounts.
when you refetch a single row, you:
refetch the one detail query, as you said, easy
onSuccess update the query data of the list with the new data from that detail query (with queryClient.setQueryData)
of course, all of this would be easier if you just had one list query and operate everything on that, but than you can't do individual refetches of one row - you'd always refetch the whole list. Usually, that's not too bad either. With the above approach, you get a bit into "syncing" state between the detail and the list query - also not so nice.
Related
I am building a page in React using Apollo Client to make GQL requests to the backend (that I'm also making). I frequently use this pattern for rendering data in tables where I have one query that gets only the Id's of the rows I want to render. I then loop over each Id and give that to a 'Row' to render by making a gql request for it's data. I like this pattern because it allows me to nest tables of data inside the table with no additional tweaking.
Example table with arrows to show or hide more information
export default function ExTable(){
// Get just the ids of the rows that should be rendered
const { data } = useGetTableDataIds()
const rows = data.queryData ?? []
return (
<table>
{
rows.map(r=><TableRow key={`row-${r}`} id={r}/>)
}
</table>
)
}
function TableRow({id}:{id:number}){
// Load data for provided id
const { data } = useGetRowData({variables: {id}})
// Render row using data ...
return <tr/>
}
What I don't like about this is all of the requests for data are made individually. I would prefer one request for all of the data. The thing I don't like is that if this data request lives at the top of the table it might be more difficult to prevent the whole table from re-rendering.
Are there any best practices on requesting data that may be frequently mutated for tables? What about when that data has a lot of optionally loaded nested data inside of it?
I want to update a record in a database. So far EditTodoList is updating the corresponding record in the db perfectly. My problem is with the optimistic update part the changes only show after refetching "TODO_LIST"+projectId query, even though this useMutation setup I have is supposed to be reflect the changes instantly in the ui.
I didn't face this problem when creating the todoList because in onMutate I just needed to do this queryClient.setQueryData("TODO_LIST"+projectId,(old)=>[...old,newTodoList]), but this case I need to change old array.
return useMutation(
EditTodoList,
{
onSuccess:(newProject)=>queryClient.setQueryData("TODO_LIST"+projectId,(old)=>[...old,newProject]),
onMutate:(values)=>{
const {title,projectId,todos,todoListToBeEdited}=values
queryClient.cancelQueries("TODO_LIST"+projectId)
const previousData=queryClient.getQueryData("TODO_LIST"+projectId)
//update query data
let temp=[...previousData]
const targetTodoList=temp.filter(tds=>tds.id === todoListToBeEdited.id)[0]
const targetTodoListIndex=temp.indexOf(targetTodoList)
const newTodoList = TodoListModel(projectId,title,todoListToBeEdited.orderInProject)
temp[targetTodoListIndex] = newTodoList
//this is where I need help , the setQueryData seems to ignore temp even though temp has the latest up-to-date data
queryClient.setQueryData("TODO_LIST"+projectId,(old)=>[...temp])
return queryClient.setQueryData("TODO_LIST"+projectId,previousData)
},
onError:(err,values,rollBack)=>rollBack()
}
)
the suggested way is:
optimistically update in onMutate with setQueryData. If you have a list, yes, it means iterating over that list and finding the right element to update.
return something to rollback
rollback on error
invalidate onSettled to refetch the query in any case to be in-sync with the server state. This is optional, if your optimistic update is "perfect" (like just toggling a boolean), there's sometimes no need to do that.
There's a whole section about optimistic updates in the docs, and there are also codesandbox examples
/edit: sorry, I missed the comment about the setQueryData. If you have computed the next data already, you don't need to use the functional updater. This should work:
queryClient.setQueryData("TODO_LIST"+projectId, temp)
I want to do some side effects like setState and update context after the data is fetched. However, the onSuccess will not be executed when the data is in cache. Also useEffect doesn't work because if the data is cached, it doesn't change from undefined to the real data. Therefore it doesn't get trigger either. What's the best way of doing this? Thanks
My usecase is to extract some values from the data returned from useQuery and set a new state on those.
usually, they’re shouldn’t be a need to be a need to copy state from react-query into local state. This will just lead to duplication of the source of truth. It is best to keep them separated, so that you can also profit from background updates.
If you want to transform the data or subscribe to parts of the data, use the select option of useQuery:
const { data } = useQuery(key, fn, { select: data => data.map(...) })
Alternatively, you can compute some new data depending on the returned data with useMemo, e.g.:
const { data } = useQuery(...)
const articles = useMemo(() => data?.map(...), [data])
// work with articles from here on
You can also put that nicely in a custom hook.
I try to use ApolloClient 2.1 with the new Mutation Component.
Simple use cases are working but now I have something more complex.
What I want to achieve is to query data and put them in a list, then sort this list (here via react-sortable-hoc) and once sorted, I want to update the new position for all elements in the list.
So the basis is something like this, which is working for simple Querying:
const query = gql`
{
items( order:{by:"position", direction:"desc"}) {
id
name
position
}
}`
const ItemView extends Component {
onSortEnd = ({ oldIndex, newIndex }) => {
console.log("Sort ended: ", oldIndex, newIndex);
}
render() {
<Query query={query}>
{({ loading, data, error }) => {
if (loading) return <p>Loading...</p>;
if (error) return <p>Error</p>;
return (
<ItemList items={data.items} onSortEnd={this.onSortEnd} />
)
}}
</Query>
}
}
Now I really struggle at a lot of parts in order to do the mutation.
I think I need to wrap the Mutation Component. But how can I provide a GraphQL Query in there, because I want to do batch mutation with a similar query being fired multiple times, such as
mutation {
updateItem1: updateItem(id: 457092155, input: {position: 1}) {
item {
id
}
ok
errors
},
updateItem2: updateItem(id: 54489270, input: {position: 2}) {
item {
id
}
ok
errors
},
... // much more mutations, one for each item in the list
}
So my main question is, how do I pass a GraphQL mutation with dynamic amount of mutations to the Mutation component? Or should I do this completely differently?
Thanks a lot for any hints
You will have to compose multiple Mutations together in order to achieve this. You can use react-adopt for this. They have even addressed this here https://github.com/pedronauck/react-adopt#leading-with-multiple-params.
You can also take a look at the discussion going on here https://github.com/apollographql/react-apollo/issues/1867 and jasonpaulos has an example demonstrating this with hooks
Hi everyone! I believe that the new Apollo hooks, useQuery,
useMutation, and useSubscription, adequately address this use case. To
demonstrate this, I have converted #Cridda's example that uses
react-adopt and modified it to use #apollo/react-hooks here:
https://codesandbox.io/s/apollo-and-react-hooks-4vril
This example is by no means perfect, but it serves as a demonstration
of how hooks can massively simplify some use cases.
Hope this helps!
As Hemant mentioned already, the #compose annotation in Apollo 2.1 is the "correct" / conventional way to solve this problem. If that doesn't work for you for whatever reason, there is possibly another cruder/hacky way to accomplish this:
If your Item model has a parent model, you can mutate multiple nodes with one mutation by passing the children in as the array values to the connect / create / update actions.
The unfortunate limitation here is that there is no way to individually identify child nodes to be updated. What I mean is that you can filter child Items to be mutated based on a criteria (like postition = 2) but that will only allow you to mutate the filtered items to the same state; you won't be able to update them differently from one another this way.
If we allow ourselves one more crude step, you can delete the Item nodes that you wish to update before calling the update mutation - this will allow you to call the mutation with all of the updated items under the create: key in the mutation, which will allow you to specify each item to be created. In this way, the number of items you can create is only limited by the size of your request payload.
There are many cases where deleting and creating nodes is unacceptable (as opposed to updating them)...if you use this method then be sure there are no negative side effects to your use case(s) from deleting item data in this way.
I have a paginated query that returns a list of items...
query snippet:
const MY_POTENTIAL_VOTERS = gql`
query potentialVoters(...) {
potentialVoters(...)
{
items {
id
....
}
pageInfo {
nextCursor
totalCount
}
}
}
`;
In react I then map over the data.potentialVoters.items and render them to UI. The UI then allows an action that will perform a mutation against one of the items in the page.
Mutation snippet:
const ASSOCIATE_PV_VOTER = gql`
mutation updatePotentialVoter($pv_id: String!, $voter_id: String) {
updatePotentialVoter(id: $pv_id, data: { state_file_id: $voter_id }) {
id
...
}
}
`;
When exploring the devtools it appears the cache for the affected item is updating correctly with the mutated info, however that row UI (from the page query) does not update as I might expect. Even with using refetchQueries option even them sometimes the mutation is not reflected in the list render, unless I hard refresh.
I have figured out one work around to my problem but I suspect it may not be a good idea. If my page query passes down just the ID of the item to a separate component which in turns uses it’s own query to get the data for that item, I think it is making smart use of the cache and everything updates as expected, but that seems like an anti-pattern that may lead to double fetching.
Might I be doing something wrong with the way I am performing the paginated query that does not allow a react update if a list member item is updated in cache? Is my workaround a really bad idea?
I refactored the page query from using Apollo HOC to the new Query component and that seems to fix everything. I was able to then use the update prop of the Mutation component to overwrite the mutatated object in the array of page items, which now causes my page to update as expected.