Testing Optimistic update in react query - reactjs

I am trying to write the test case for an optimistic update in react query. But it's not working. Here is the code that I wrote to test it. Hope someone could help me. Thanks in advance. When I just write the onSuccess and leave an optimistic update, it works fine but here it's not working. And how can we mock the getQueryData and setQueryData here?
import { act, renderHook } from "#testing-library/react-hooks";
import axios from "axios";
import { createWrapper } from "../../test-utils";
import { useAddColorHook, useFetchColorHook } from "./usePaginationReactQuery";
jest.mock("axios");
describe('Testing custom hooks of react query', () => {
it('Should add a new color', async () => {
axios.post.mockReturnValue({data: [{label: 'Grey', id: 23}]})
const { result, waitFor } = renderHook(() => useAddColorHook(1), { wrapper: createWrapper() });
await act(() => {
result.current.mutate({ label: 'Grey' })
})
await waitFor(() => result.current.isSuccess);
})
})
export const createTestQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
cacheTime: Infinity,
},
},
logger: {
log: console.log,
warn: console.warn,
error: () => {},
}
});
export function createWrapper() {
const testQueryClient = createTestQueryClient();
return ({ children }) => (
<QueryClientProvider client={testQueryClient}>
{children}
</QueryClientProvider>
);
}
export const useAddColorHook = (page) => {
const queryClient = useQueryClient()
return useMutation(addColor, {
// onSuccess: () => {
// queryClient.invalidateQueries(['colors', page])
// }
onMutate: async color => {
// newHero refers to the argument being passed to the mutate function
await queryClient.cancelQueries(['colors', page])
const previousHeroData = queryClient.getQueryData(['colors', page])
queryClient.setQueryData(['colors', page], (oldQueryData) => {
return {
...oldQueryData,
data: [...oldQueryData.data, { id: oldQueryData?.data?.length + 1, ...color }]
}
})
return { previousHeroData }
},
onSuccess: (response, variables, context) => {
queryClient.setQueryData(['colors', page], (oldQueryData) => {
console.log(oldQueryData, 'oldQueryData', response, 'response', variables, 'var', context, 'context', 7984)
return {
...oldQueryData,
data: oldQueryData.data.map(data => data.label === variables.label ? response.data : data)
}
})
},
onError: (_err, _newTodo, context) => {
queryClient.setQueryData(['colors', page], context.previousHeroData)
},
onSettled: () => {
queryClient.invalidateQueries(['colors', page])
}
})
}

The error that you are getting actually shows a bug in the way you've implemented the optimistic update:
queryClient.setQueryData(['colors', page], (oldQueryData) => {
return {
...oldQueryData,
data: [...oldQueryData.data, { id: oldQueryData?.data?.length + 1, ...color }]
}
})
what if there is no entry in the query cache that matches this query key? oldQueryData will be undefined, but you're not guarding against that, you are spreading ...oldQueryData.data and this will error out at runtime.
This is what happens in your test because you start with a fresh query cache for every test.
An easy way out would be, since you have previousHeroData already:
const previousHeroData = queryClient.getQueryData(['colors', page])
if (previousHeroData) {
queryClient.setQueryData(['colors', page], {
...previousHeroData,
data: [...previousHeroData.data, { id: previousHeroData.data.length + 1, ...color }]
}
}
If you are using TanStack/query v4, you can also return undefined from the updater function. This doesn't work in v3 though:
queryClient.setQueryData(['colors', page], (oldQueryData) => {
return oldQueryData ? {
...oldQueryData,
data: [...oldQueryData.data, { id: oldQueryData?.data?.length + 1, ...color }]
} : undefined
})
This doesn't perform an optimistic update then though. If you know how to create a valid cache entry from undefined previous data, you can of course also do that.

Related

MockedProvider does not work when Component query changes

I have simplified problem into these files
TestComp.tsx
export const TestComp: FC<> = () => {
const [testValue, setTestValue] = React.useState(5);
const where = React.useMemo(() => testValue, [testValue]);
const { data: productsData } = useQuery<QueryProductsResult>(QUERY_PRODUCTS, {
variables: {
where,
},
});
setTimeout(() => {
setTestValue(7);
}, 0);
console.log(productsData, where);
return <div>content</div>;
};
TestComp.test.tsx
const productsData = [
{
... some data ...
},
];
const mockContractPricing = [
{
request: {
query: QUERY_PRODUCTS,
variables: {
where: 7,
},
},
result: {
data: {
products: productsData,
},
},
},
];
describe('dsfgdas', () => {
it('ggg', async () => {
const { container } = render(
<MockedProvider mocks={mockContractPricing} addTypename={false}>
<TestComp />
</MockedProvider>
);
await act(() => new Promise((resolve) => setTimeout(resolve, 0)));
console.log(container.innerHTML);
});
});
So when where is updated it does not work. If I put where=7 since in the beginning it does not work. But if it is changing inside the component it never works out. Even trying to make it where: 7 in the mocked query it doesn't work.
The setTimeout is put only for demonstration purpose.
Is there any way to do it?
Console output:
console.log
undefined 5
console.log
undefined 7
console.log
content

React-query: typescript and abstract useMutations?

I have a small app with several kinds of data similar with each other. For example data for labels and statuses. And so the apis are similar too.
Now with react-query, I'm writing many repetitive mutations. All the mutations (add, update, delete) have same structure:
export const useUpdateLabel = () => {
const queryClient = useQueryClient();
return useMutation(updateLabel, {
onSuccess: () => {
queryClient.invalidateQueries("labels");
console.log(`Updated`);
},
onError: (error) => {
process.env.NODE_ENV !== "production" && console.error(error);
},
});
};
I'm using custom hooks to make the code cleaner, but is there any way to reduce the repetitive codes?
I can do something like:
export const useCustomMutation = (func, key) => {
const queryClient = useQueryClient();
return useMutation(func, {
onSuccess: () => {
queryClient.invalidateQueries(key);
console.log(`Updated`);
},
onError: (error) => {
process.env.NODE_ENV !== "production" && console.error(error);
},
});
};
but have no idea how to make the types right.
this should work nicely:
import { useMutation, useQueryClient, QueryKey } from 'react-query'
export const useCustomMutation = <TArguments, TResult>(func: (args: TArguments) => Promise<TResult>, key: QueryKey) => {
const queryClient = useQueryClient();
return useMutation(func, {
onSuccess: () => {
queryClient.invalidateQueries(key);
console.log(`Updated`);
},
onError: (error) => {
process.env.NODE_ENV !== "production" && console.error(error);
},
});
};
link to typescript playground

how to call useMutation hook from a function?

I have this functional React component:
// CreateNotification.tsx
import {useMutation} from '#apollo/client';
import resolvers from '../resolvers';
const createNotification = (notification) => {
const [createNotification] = useMutation(resolvers.mutations.CreateNotification);
createNotification({
variables: {
movie_id: notification.movie.id,
actor_id: notification.user.id,
message:
`${notification.user.user_name} has added ${notification.movie.original_title} to their watchlist.`,
},
});
};
export default createNotification;
I call the createNotification component in a function and pass in some variables after a other useMutation hook has been called:
// AddMovie.tsx
const addMovie = async (movie: IMovie) => {
await addUserToMovie({
variables: {...movie, tmdb_id: movie.id},
update: (cache, {data}) => {
cache.modify({
fields: {
moviesFromUser: () => {
return [...data.addUserToMovie];
},
},
});
},
}).then( async () => {
createNotification({movie: movie, user: currentUserVar()});
});
};
When I run the code I get the (obvious) error:
Uncaught (in promise) Error: Invalid hook call. Hooks can only be called inside of the body of a function component
Because I call the createNotification hook in the addMovie function.
If I move the createNotification to the top of the component:
// AddMovie.tsx
const AddMovieToWatchList = ({movie}: {movie: IMovie}) => {
createNotification({movie: movie, user: currentUserVar()});
const [addUserToMovie] = useMutation(resolvers.mutations.AddUserToMovie);
const addMovie = async (movie: IMovie) => {
await addUserToMovie({
variables: {...movie, tmdb_id: movie.id},
update: (cache, {data}) => {
cache.modify({
fields: {
moviesFromUser: () => {
return [...data.addUserToMovie];
},
},
});
},
});
};
}
The code works fine, except that the hook is now called every time the AddMovie component is rendered instead of when the addMovie function is called from the click:
return (
<a className={classes.addMovie} onClick={() => addMovie(movie)}>
Add movie to your watchlist
</a>
);
Figured it out:
// createNotification.tsx
import {useMutation} from '#apollo/client';
import resolvers from '../resolvers';
export const createNotification = () => {
const [createNotification, {data, loading, error}] = useMutation(resolvers.mutations.CreateNotification);
const handleCreateNotification = async (notification) => {
createNotification({
variables: {
movie_id: notification.movie.id,
actor_id: notification.user.id,
message:
`${notification.user.user_name} has added ${notification.movie.original_title} to their watchlist.`,
},
});
console.log(data, loading, error);
};
return {
createNotification: handleCreateNotification,
};
};
If I'm correct then this returns a reference (createNotification) to the function handleCreateNotification
Then in the component I want to use the createNotification helper I import it:
// AddMovie.tsx
import {createNotification} from '../../../../helpers/createNotification';
const AddMovieToWatchList = ({movie}: {movie: IMovie}) => {
const x = createNotification();
const addMovie = async (movie: IMovie) => {
await addUserToMovie({
variables: {...movie, tmdb_id: movie.id},
update: (cache, {data}) => {
cache.modify({
fields: {
moviesFromUser: () => {
return [...data.addUserToMovie];
},
},
});
},
}).then( async () => {
x.createNotification({movie: movie, user: currentUserVar()});
});
}
};
You (kind of) answer your own question by showing the error and saying it's obvious. createNotification is not a React component, and it is not a custom hook, it is just a function. Thus using a hook inside of it breaks the Rules of Hooks.
If you want to keep that logic in it's own function, that's fine, just redefine your component like this:
const AddMovieToWatchList = ({movie}: {movie: IMovie}) => {
const [addUserToMovie] = useMutation(resolvers.mutations.AddUserToMovie);
const [createNotification] = useMutation(resolvers.mutations.CreateNotification);
const addMovie = async (movie: IMovie) => {
await addUserToMovie({
variables: {...movie, tmdb_id: movie.id},
update: (cache, {data}) => {
cache.modify({
fields: {
moviesFromUser: () => {
return [...data.addUserToMovie];
},
},
});
},
});
await createNotification({movie: movie, user: currentUserVar()});
};
return (
<a className={classes.addMovie} onClick={() => addMovie(movie)}>
Add movie to your watchlist
</a>
);
}

react-apollo GraphQL query - this.client.watchQuery is not a function

export default graphql(queryShippingRates, {
options: props => ({
variables: {
id: props.navigation.state.params.checkout.id
},
pollInterval: 5000,
client: {
apolloclient: props.navigation.state.params.apolloclient
}
}),
props: ({
data
}) => {
if (data.loading) {
return {
fetchNextPage: () => {}
};
} else if (!data.node.availableShippingRates.ready) {
return {
fetchNextPage: () => {}
};
} else if (!data) {
return {
fetchNextPage: () => {}
};
} else if (data.error) {
console.warn('error', data.error);
}
const fetchNextPage = () => {
return data.fetchMore({
updateQuery: (previousResult, {
fetchMoreResult
}) => {
return {
node: fetchMoreResult.data.node,
};
},
});
};
console.log(fetchNextPage);
return {
data: data,
};
},
})(CheckoutShippingMethodScreen);
I am using the code above to send a graphQL query via react-apollo. As you can see above, I placed a prop within options to specify which client to use when sending the Query. When I run the code, I get an error saying that this.client.watchQuery is not a function.
I have the app wrapped by an ApolloProvider Component with a specified client, but I read that you can place a custom client as a prop. Can someone please help me with this?

debounce async/await and update component state

I have question about debounce async function. Why my response is undefined? validatePrice is ajax call and I receive response from server and return it (it is defined for sure).
I would like to make ajax call after user stops writing and update state after I get reponse. Am I doing it right way?
handleTargetPriceDayChange = ({ target }) => {
const { value } = target;
this.setState(state => ({
selected: {
...state.selected,
Price: {
...state.selected.Price,
Day: parseInt(value)
}
}
}), () => this.doPriceValidation());
}
doPriceValidation = debounce(async () => {
const response = await this.props.validatePrice(this.state.selected);
console.log(response);
//this.setState({ selected: res.TOE });
}, 400);
actions.js
export function validatePrice(product) {
const actionUrl = new Localization().getURL(baseUrl, 'ValidateTargetPrice');
return function (dispatch) {
dispatch({ type: types.VALIDATE_TARGET_PRICE_REQUEST });
dispatch(showLoader());
return axios.post(actionUrl, { argModel: product }, { headers })
.then((res) => {
dispatch({ type: types.VALIDATE_TARGET_PRICE_REQUEST_FULFILLED, payload: res.data });
console.log(res.data); // here response is OK (defined)
return res;
})
.catch((err) => {
dispatch({ type: types.VALIDATE_TARGET_PRICE_REQUEST_REJECTED, payload: err.message });
})
.then((res) => {
dispatch(hideLoader());
return res.data;
});
};
}
Please find below the working code with lodash debounce function.
Also here is the codesandbox link to play with.
Some changes:-
1) I have defined validatePrice in same component instead of taking from prop.
2) Defined the debounce function in componentDidMount.
import React from "react";
import ReactDOM from "react-dom";
import _ from "lodash";
import "./styles.css";
class App extends React.Component {
state = {
selected: { Price: 10 }
};
componentDidMount() {
this.search = _.debounce(async () => {
const response = await this.validatePrice(this.state.selected);
console.log(response);
}, 2000);
}
handleTargetPriceDayChange = ({ target }) => {
const { value } = target;
console.log(value);
this.setState(
state => ({
selected: {
...state.selected,
Price: {
...state.selected.Price,
Day: parseInt(value)
}
}
}),
() => this.doPriceValidation()
);
};
doPriceValidation = () => {
this.search();
};
validatePrice = selected => {
return new Promise(resolve => resolve(`response sent ${selected}`));
};
render() {
return (
<div className="App">
<input type="text" onChange={this.handleTargetPriceDayChange} />
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Hope that helps!!!
You can use the throttle-debounce library to achieve your goal.
Import code in top
import { debounce } from 'throttle-debounce';
Define below code in constructor
// Here I have consider 'doPriceValidationFunc' is the async function
this.doPriceValidation = debounce(400, this.doPriceValidationFunc);
That's it.

Resources