Custom useMutation hook with Apollo Client and React - javascript

When using Apollo client, I find it quite tedious to manually update the cache for every mutation that requires an immediate UI update. I therefore decided to try to make a custom hook which updates the cache automatically.
The hook works but it seems a little "hacky" and I'm worried it might mess with the normal functioning of the cache. So I just wanted to ask if this hook seems like it should work ok?
Here's the code (where mutationName is the actual graphql mutation name and fieldName is the original graphql query name corresponding to the mutation):
export const useMutationWithCacheUpdate = (
mutation,
mutationName,
fieldName
) => {
const [createMutation, { data, loading, error }] = useMutation(mutation, {
update(cache, { data }) {
data = data[mutationName];
cache.modify({
fields: {
[fieldName]: (existingItems = []) => {
const newItemRef = cache.writeFragment({
data: data,
fragment: gql`
fragment newItem on ${fieldName} {
id
type
}
`,
});
return [...existingItems, newItemRef];
},
},
});
},
});
return [createMutation, { data, loading, error }];
};

Related

How to invalidate from React-Query mutation?

I am trying to refetch a specific data, which should update after a post request in another API. It's worth mentioning that based on that request two APIs should update and one of them updates, another one not.
I've tried multiple ways to refetch the API based on successful post request, but nothing seems to work.
I tried to also add 2nd option like this, but without success.
{
refetchInactive: true,
refetchActive: true,
}
As it mentioned in this discussion,
maybe "keys are not matching, so you are trying to invalidate something that doesn't exist in the cache.", but not sure that it's my case.
What is more interesting is that clicking the invalidate button in the devtools, the invalidation per se works.
Mutation function:
import { BASE_URL, product1Url, product2Url } from './services/api'
import axios from 'axios'
import { useMutation, useQueryClient } from 'react-query'
export const usePostProduct3 = () => {
const queryClient = useQueryClient()
const mutation = useMutation(
async (data) => {
const url = `${BASE_URL}/product3`
return axios.post(url, data).then((response) => response)
},
{
onSuccess: async (data) => {
return (
await queryClient.invalidateQueries(['prodKey', product1Url]), //this one doesn't work
queryClient.invalidateQueries(['prodKey', product2Url]) //this api refetch/update works
) },
},
)
return mutation
}
What am I doing wrong?
You shold do it like this
onSuccess:(response)=>{
queryClient.setQueryData("your query key",response.data)
}
Or if you just want to invalidate queries
import {querCache} from "react-query"
...
onSuccess:(response)=>{
queryClient.invalidateQueries("your query key")
queryClient.invalidateQueries("your query key")
//you also can refetch data like this
queryCache.refetchQueries("your query key")
}

Unable to implement React-table sever data loading

I am testing the React-table server side data to render a huge amount of data fetched from an web api without crashing the browser. With the base react-table settings the browser is unable to handle such amount of records (500000) and crash (it gets stuck in the pending state of the request).
So I find the server side data that maybe can help me.
I followed the instructions from the documentation but typescript is complaining about the data that I am trying to use when I update the state.
This is what I have until now:
The method that fetch the data from web api:
private fetchSales() {
fetch(`http://localhost:50335/api/RK`)
.then(response => response.json())
.then(data =>
this.setState({
sales: data // here I get 500000 items
})
)
}
This fetchSales gets called in the componentDidMount().
Then I have the ReactTable component inside the render():
render() {
const {
sales,
pages
} = this.state;
return (
<div className = "App" >
<ReactTable
data = {sales}
manual
pages = {pages}
defaultPageSize = {10}
onFetchData = {this._fetchData}
columns = {
[{
Header: "Region",
accessor: "Region"
},
{
Header: "Country",
accessor: "Country"
}]
}
/>
</div>
);
}
In the ReactTable there is call to a function called _fetchData and that function looks like this:
private _fetchData(state: any) {
requestData(
state.sales,
state.pageSize,
state.page
).
then(res => {
this.setState({
sales: res.rows, // here typescript complain: "res is of type 'unknown'"
pages: res.pages // here typescript complain: "res is of type 'unknown'"
});
})
}
Inside the setState the res object is type 'unknown' and typescript doesn't like it.
requestData is a function that lives outside the class and get the sales, pageSize and page states:
const requestData = (sales: any, page: number, pageSize: number) => {
return new Promise((resolve, reject) => {
const res = {
rows: sales.slice(pageSize * page, pageSize * page * pageSize),
pages: Math.ceil(sales.length / pageSize)
}
resolve(res);
})
}
The function is almost identical the in the documentation I only removed the filtering and sorting because I don't need them. I only need the res object that return the function.
And I almost forget it, inside the constructor I am attaching the this to the _fetchData method: this._fetchData = this._fetchData.bind(this);
Why is typescript complaining about the res object that I am trying to use to set the state?
Best regards!
Americo
EDIT
I noticed a few mistakes.
private _fetchData(state: any) {
requestData(
state.sales, // that should be state.data (state is the state of ReactTable)
state.pageSize,
state.page
...
Next, I've noticed that you do pagination after fetching the data. But using manual with onFetchData is for handling pagination on the server. For example, with page and pageSize you could pass these parameters to your API. That's the whole point of pagination!
An example from the documentation:
onFetchData={(state, instance) => { //onFetchData is called on load and also when you click on 'next', when you change page, etc.
// show the loading overlay
this.setState({loading: true})
// fetch your data
Axios.post('mysite.com/data', {
//These are all properties provided by ReactTable
page: state.page,
pageSize: state.pageSize,
sorted: state.sorted,
filtered: state.filtered
})
Since you're fetching all of them at once (why though? can't your API on the server handle pagination? this way, you won't have to wait ages before the results are returned), then I suggest you let ReactTable do the work.
That is, you just do:
<ReactTable
columns={columns}
data={data}
/>
ReactTable will take care of pagination. And now, you may use your query to the API in ComponentDidMount.
Could you please instead try to put all the logic in the onFetchData. I could be wrong but it seems to me you misunderstood the instructions in the documentation. OnFetchData is called at ComponentDidMount, it's not telling you that you have to put your function there.
private _fetchData(state, instance) {
const { page, pageSize } = state
fetch(`http://localhost:50335/api/RK`)
.then(response => response.json())
.then(data =>
let sales = data
this.setState({
sales: sales.slice(pageSize * page, pageSize * page * pageSize),
pages: Math.ceil(sales.length / pageSize)
});
)
}
As for Typescript, from what I gather, Typescript doesn't have enough information to infer the type of what your Promise returns.
So you have to explicitly annotate Promises generic type parameter:
return new Promise<{ sales: object; pages: number; }>((resolve, reject) => { ... }

How to keep data synchronized in ember using ember-apollo-client?

I have an app built using Ember and ember-apollo-client.
// templates/collaborators.hbs
// opens an ember-bootstrap modal
{{#bs-button type="success" onClick=(action (mut createCollaborator) true)}}Create collaborator{{/bs-button}}
// submit button in modal triggers "createCollaborator" in controller
{{#each model.collaborators as |collaborator|}}
{{collaborator.firstName}} {{collaborator.lastName}}
{{/each}}
// routes/collaborators.js
import Route from '#ember/routing/route';
import { RouteQueryManager } from 'ember-apollo-client';
import query from '../gql/collaborators/queries/listing';
export default Route.extend(RouteQueryManager, {
model() {
return this.get('apollo').watchQuery({ query });
}
});
// controllers/collaborator.js
export default Controller.extend({
apollo: service(),
actions: {
createCollaborator() {
let variables = {
firstName: this.firstName,
lastName: this.lastName,
hireDate: this.hireDate
}
return this.get('apollo').mutate({ mutation, variables }, 'createCollaborator')
.then(() => {
this.set('firstName', '');
this.set('lastName', '');
this.set('hireDate', '');
});
}
}
});
Currently, after creating a collaborator the data is stale and needs a browser refresh in order to update. I'd like the changes to be visible on the collaborators list right away.
From what I understood, in order to use GraphQL with Ember, I should use either Ember Data with ember-graphql-adapter OR just ember-apollo-client. I went on with apollo because of its better documentation.
I dont think I quite understood how to do that. Should I somehow use the store combined with watchQuery from apollo? Or is it something else?
LATER EDIT
Adi almost nailed it.
mutationResult actually needs to be the mutation itself.
second param in store.writeQuery should be either data: { cachedData } or data as below.
Leaving this here as it might help others.
return this.get('apollo').mutate({
mutation: createCollaborator,
variables,
update: (store, { data: { createCollaborator } }) => {
const data = store.readQuery({ query })
data.collaborators.push(createCollaborator);
store.writeQuery({ query, data });
}
}, createCollaborator');
You can use the apollo imperative store API similar to this:
return this.get('apollo').mutate(
{
mutation,
variables,
update: (store, { data: {mutationResult} }) => {
const cachedData = store.readyQuery({query: allCollaborators})
const newCollaborator = mutationResult; //this is the result of your mutation
store.writeQuery({query: allCollaborators, cachedData.push(newCollaborator)})
}
}, 'createCollaborator')

How can I add computed state to graph objects in React Apollo?

I really like the graphQL pattern of having components request their own data, but some data properties are expensive to compute and so I want to localize the logic (and code) to do so.
function CheaterList({ data: { PlayerList: players } }) {
return (
<ul>
{players && players.map(({ name, isCheater }) => (
<li key={name}>{name} seems to be a {isCheater ? 'cheater' : 'normal player'}</li>
))}
</ul>
);
}
export default graphql(gql`
query GetList {
PlayerList {
name,
isCheater
}
}
`)(CheaterList);
The schema looks like:
type Queries {
PlayerList: [Player]
}
type Player {
name: String,
kills: Integer,
deaths: Integer
}
And so I want to add the isCheater property to Player and have its code be:
function computeIsCheater(player: Player){
// This is a simplified version of what it actually is for the sake of the example
return player.deaths == 0 || (player.kills / player.deaths) > 20;
}
How would I do that?
Another way of phrasing this would be: how do I get the isCheater property to look as though it came from the backend? (However, if an optimistic update were applied the function should rerun on the new data)
Note: Local state management is now baked into apollo-client -- there's no need to add a separate link in order to make use of local resolvers and the #client directive. For an example of mixing local and remote fields, check out of the docs. Original answer follows.
As long as you're using Apollo 2.0, this should be possible by utilizing apollo-link-state as outlined in the docs.
Modify your client configuration to include apollo-link-state:
import { withClientState } from 'apollo-link-state';
const stateLink = withClientState({
cache, //same cache object you pass to the client constructor
resolvers: linkStateResolvers,
});
const client = new ApolloClient({
cache,
link: ApolloLink.from([stateLink, new HttpLink()]),
});
Define your client-only resolvers:
const linkStateResolvers = {
Player: {
isCheater: (player, args, ctx) => {
return player.deaths == 0 || (player.kills / player.deaths) > 20
}
}
}
Use the #client directive in your query
export default graphql(gql`
query GetList {
PlayerList {
name
kills
deaths
isCheater #client
}
}
`)(CheaterList);
However, it appears that combining local and remote fields in a single query is currently broken. There's an open issue here that you can track.

Creating a altjs flux store for fetching data from API

I'm stuck trying to figure out how to write a flux store and action that works in just fetching data from my express API using altjs
import $ from 'jquery';
const utils = {
myProfile: () => {
return $.ajax({
url: '/myProfile',
type: 'GET'
});
}
};
This is how I believe I should write my GET request for just grabbing a user's profile (which should return a json with user info).
then for my store :
import UserActions from 'actions/UserActions';
import alt from 'altInstance';
class UserStore {
constructor() {
this.userProfile = [];
this.on('init', this.bootstrap);
this.on('bootstrap', this.bootstrap);
this.bindListeners({
fetchUserProfile: UserActions.FETCHUSERPROFILE,
});
}
fetchUserProfile(profile) {
this.userProfile = profile;
}
}
export default alt.createStore(UserStore, 'UserStore');
However the action is where i'm the most clueless
import alt from 'altInstance';
import UserWebAPIUtils from 'utils/UserWebAPIUtils';
fetchProfile(){
this.dispatch();
UserWebAPIUtils.getProfile()
//what do we do with it to let our store know we have the data?
});
}
}
}
All im trying to do, is grab data from the server, tell my store we've recieved the data and fill the userprofile array with the data from our api, and the messenger for telling our store is through a dispatcher which belongs to 'actions' correct? I've looked at a lot of tutorials but I still dont feel very confident on how I am thinking about this. What if I wanted to update data through a POST request what would that be like?
Looking through altjs doc it seems like they recommend doing the async operations from actions. I prefer this approach as well because it keeps stores synchronous and easy to understand. Based on their example
LocationAction
LocationsFetcher.fetch()
.then((locations) => {
// we can access other actions within our action through `this.actions`
this.actions.updateLocations(locations);
})
.catch((errorMessage) => {
this.actions.locationsFailed(errorMessage);
});
Basically they are fetching the information and then triggering 2 actions depending on the result of the request which the store is listening on to.
LocationStore
this.bindListeners({
handleUpdateLocations: LocationActions.UPDATE_LOCATIONS,
handleFetchLocations: LocationActions.FETCH_LOCATIONS,
handleLocationsFailed: LocationActions.LOCATIONS_FAILED
});
When the store receives a handleUpdateLocations action which happens when the fetcher returns successfully. The store will update itself with new data and dispatch
handleUpdateLocations(locations) {
this.locations = locations;
this.errorMessage = null;
}
With your code you can do something similar. The fetch user profile will be triggered when data is originally requested. Here I am setting user profile to be [] which is your original init value but you can set it to anything to indicate data is being loaded. I then added 2 more methods, handleFetchUserProfileComplete and handleFetchUserProfileError which get called depending on if your fetch was successful or not. The code below is a rough idea of what you should have.
constructor() {
this.userProfile = [];
this.on('init', this.bootstrap);
this.on('bootstrap', this.bootstrap);
this.bindListeners({
handleFetchUserProfile: UserActions.FETCH_USER_PROFILE,
handleFetchUserProfileComplete: UserActions.FETCH_USER_PROFILE_COMPLETE,
handleFetchUserProfileError: UserActions.FETCH_USER_PROFILE_ERROR,
});
}
fetchUserProfile() {
this.userProfile = [];
}
handleFetchUserProfileComplete(profile) {
this.userProfile = profile;
}
handleFetchUserProfileError(error) {
this.error= error;
}
export default alt.createStore(UserStore, 'UserStore');
The only thing left is to trigger these 2 actions depending on the result of your fetch request in your action code
fetchUserProfile(){
this.dispatch();
UserWebAPIUtils.getProfile().then((data) => {
//what do we do with it to let our store know we have the data?
this.actions.fetchUserProfileComplete(data)
})
.catch((errorMessage) => {
this.actions.locationsFailed(errorMessage);
});
}
fetchUserProfileComplete(profile) {
this.dispatch(profile);
}
fetchUserProfileError(error) {
this.dispatch(error);
}

Categories