How to enhance the response in apollo client with react only once - reactjs

I make a GraphQL query where I get back a lot of data and than I calculate the min and max values. As the calculating is quite time consuming I would like to only do it when I receive the value. Unfortunately the props method is called every time the component is rerendered even there was no new call, and the data comes from the store. How can I limit the calculation to the points where I really get new data
graphql(DataQuery, {
options: ({ id }) => ({
variables: {
id,
},
}),
props: ({ data: { result} }) => ({
data: result,
min: getMin(result),
max: getMax(result),
}),
})

This problem is similar to the problem in redux where mapStateToProps() will be called again every time the store updates repeating costy calculations.
You can solve that by using memorized selectors:
import { createSelector } from 'reselect'
const getMinMax = createSelector(
result => result,
result => ({
min: getMin(result),
max: getMax(result),
})
)
graphql(DataQuery, {
options: ({ id }) => ({
variables: {
id,
},
}),
props: ({ data: {result} }) => ({
data: result,
...getMinMax(result), // will only re-calculate if result changed
}),
})
Memorized selectors remember the last result of a call and keep returning that on subsequent calls as long as the input does not change.

Related

How to refetch/fetch after a series of mutation in graphql/apollo/react

Right now I have a use case to use two useMutations to create/update database. So the second one is depends on the success of the first one. And also the second mutation needs to be called in a loop, just think about that I have a array and I need loop through the array and apply the second mutation.
After all these mutation finished I have to refetch another api to update caches, because the cache would be impacted by the two mutations.
I am really struggling with how to achieve this.
From another post: Apollo Client - refetchQueries after multiple updates
I can do probably like
const [creatEnrollment] = useMutation(mut1)
const [updateEnrollment] = useMutation(mut2)
const [toFetch, {loading, error, data}] = useLazyQuery(UsersDocument)
await Promise.all([creatEnrollment(), updateEnrollment()])
const result = () => toFetch({
variables: {name: 'i'}
})
but the problem is 1. I need to execute second mutations after the first one; 2, I need to have an array that applied to second mutations one by one.
I also saw
here How can I wait for mutation execution in React Query?
we can use onSuccess
const mutate1 = useMutation((data) => axios.post('/something', { data }))
const mutate2 = useMutation(somethingResult) => axios.put('/somethingElse', { somethingResult })
<button onClick={() => {
mutate1.mutate('data', {
onSuccess: mutate2.mutate
})
}} />
But still 1. how to loop thru mutate2.mutate? and how to fetch after mutate2 finished
do like this????:
<button onClick={() => {
mutate1.mutate('data', {
onSuccess: mutate2.mutate
})
mutate2.mutate('data', {
onSuccess: query
})
}} />
Thank you for helping!!
You can have a function for useMutation and onSuccess the data which use get on success use other mutation
const mutationFuntion = (id) => {
// this is first mutation
return useMutation(
(newTitle) => axios
.patch(`/posts/${id}`, { title: newTitle })
.then(response => response.data),
{
// 💡 response of the mutation is passed to onSuccess
onSuccess: (data) => {
// call the api which will get all the latest update
},
}
)
}
Get the Data of first mutation
const [addTodo, { data, loading, error }] = mutationFuntion(//send data);
This is consecutive mutation I found it from this https://react-query-v3.tanstack.com/guides/mutations#consecutive-mutations doc
useMutation(addTodo, {
onSuccess: (data, error, variables, context) => {
// Will be called 3 times
},
})
['Todo 1', 'Todo 2', 'Todo 3'].forEach((todo) => {
mutate(todo, {
onSuccess: (data, error, variables, context) => {
// Will execute only once, for the last mutation (Todo 3),
// regardless which mutation resolves first
},
})
})
For handle the promise of every mutation call
const mutation = useMutation(addTodo)
try {
const todo = await mutation.mutateAsync(todo)
console.log(todo)
} catch (error) {
console.error(error)
} finally {
console.log('done')
}
Please you need to verify on what kind of object you want to call mutation in loop it array or some thing alse.

(RTK) How to pass the query parameter from react to createSelector

I am currently using RTK query to get the event log from the database according to the date at the end of the url:
export const extendedChartApiSlice = apiSlice.injectEndpoints({
endpoints: builder => ({
getChart: builder.query({
query: (date) => `/api/eventlog/${date}`,
transformResponse: responseData => {
const loadedData = responseData['event_log']
return chartAdapter.setAll(initialState, loadedData);
},
providesTags: (result, error, arg) => [
{ type: 'Chart', id: 'LIST' },
...result.ids.map(id => ({ type: 'Chart', id }))
]
}),
})
});
For now, I am able to obtain the data with the useGetChartQuery like this:
const {
data: logEventByDate,
isLoading,
isSuccess,
isError,
error
} = useGetChartQuery(date);
However, the data obtained is the complete normalized data including the ids and entities, what I am trying to achieve is to destructure the normalized data in the slice by creating a memorized selector like this:
export const selectChartResult = extendedChartApiSlice.endpoints.getChart.select();
export const selectChartData = createSelector(
selectChartResult,
chartResult => chartResult.data
);
However, the selectChartData returned undefined. I think I need to pass the parameter to the selectChartData first, like I did with the useGetChartQuery. Has anybody got an idea what is wrong with my code?

Multiple useLazyQuery hooks (Apollo Client) in React Function Component

I am trying to include two Apollo-Client useLazyQuery hooks in my function component. Either works fine alone with the other one commented out, but as soon as I include both, the second one does nothing. Any ideas?
export default function MainScreen(props) {
useEffect(() => {
validateWhenMounting();
}, []);
const [validateWhenMounting, { loading, error, data }] = useLazyQuery(
validateSessionToken,
{
onCompleted: (data) => console.log('data', data),
},
);
const [validate, { loading: loading2, error: error2, data: data2 }] =
useLazyQuery(validateSessionTokenWhenSending, {
onCompleted: (data2) => console.log('data2', data2),
});
const handleSendFirstMessage = (selectedCategory, title, messageText) => {
console.log(selectedCategory, title, messageText);
validate();
};
Figured it out: Adding the key-value pair fetchPolicy: 'network-only', after onCompleted does the trick. It seems that otherwise, no query is being conducted due to caching...
This is the pattern that I was talking about and mentioned in the comments:
const dummyComponent = () => {
const [lazyQuery] = useLazyQuery(DUMMY_QUERY, {variables: dummyVariable,
onCompleted: data => // -> some code here, you can also accept an state dispatch function here for manipulating some state outside
onError: error => // -> you can accept state dispatch function here to manipulate state from outside
});
return null;
}
this is also a pattern that you are going to need sometimes

Dispatching function multiple times in order to execute it with different parameters

Background
I'm building a React Native 0.64.1 app using Redux 4.1.0. This app fetches data from an API endpoint via POST which can take multiple "category" params. Only one value can be passed as category at a time, so in order to display data from multiple categories one would have to execute the function one time per category.
This is how the axios request is handled:
export const getData = (tk, value) =>
apiInstance
.request({
url: ENDPOINTS.CATEGORIES,
method: 'POST',
data: qs.stringify({
token: tk,
category: value,
}),
})
.then(response => {
return response.data;
})
.catch(error => {
return Promise.reject(error.message);
});
This function is then executed via a redux action/reducer, etc.
The tricky part is that "value" is set by the user and can be changed at any point in time.
The front end meets this function in a certain screen where this happens:
useEffect(() => {
dispatch(retrieveData(tk, value));
}, [dispatch, value]);
Problem & Question
I've tried doing a for loop that would iterate through an array that contains the possible strings of text value could be, that would look something like this:
const arrayOfValues = ['A','B','C','D']
let value = null;
useEffect(() => {
for (let i = 0; i < arrayOfValues.length; i++) {
value = arrayOfValues[i];
dispatch(retrieveData(tk, value));
}
}, [dispatch, value]);
I know this is horrible and I'm just showing it because it's the only thing I could think about (and it doesn't even work).
An ideal solution would:
Execute the first request on load
Run a request once per item in an array WITHOUT deleting the previously called for data
Each time it runs it needs to update the "value" parameter.
As a note about "retrieveData()", that is just the redux action.
Any help would be very much appreciated.
Solution by #rigojr
This seems like it should work, but either I haven't expressed myself properly or there's something wrong with the answer. I'm guessing it's the former.
#rigojr proposed the following:
export const getData = (tk, values) => values.map((value) => apiInstance
.request({
url: ENDPOINTS.CATEGORIES,
method: 'POST',
data: qs.stringify({
token: tk,
category: value,
}),
}))
Promise.all(getData(tk,values)) *****
.then(responseValues => {
// Dispatch the response, it will come an array of values response.
})
.catch(eer => {
// Error handling
})
Howeve, values in the line marked with many asterisks is inaccessible. I believe this is because previosuly I failed to mention that the whole Redux data flow happens in three separate files.
Dispatching the action: UI dispatches an action onLoad in App.js:
useEffect(() => {
dispatch(retrieveData(tk, values));
}, [dispatch, value]);
The action is ran in action.js file. It looks something like this:
Note that I have added the Promise.all() in this screen, as it seems like the place where it should actually go, instead of the other one.
export const actionTypes = keyMirror({
RETRIEVE_REQUEST: null,
RETRIEVE_SUCCESS: null,
RETRIEVE_FAILURE: null,
});
const actionCreators = {
request: createAction(actionTypes.RETRIEVE_REQUEST),
success: createAction(actionTypes.RETRIEVE_SUCCESS),
failure: createAction(actionTypes.RETRIEVE_FAILURE),
};
export const retrieveData = (tk, values) => dispatch => {
dispatch(actionCreators.request());
Promise.all(getData(tk, values))
.then(data => dispatch(actionCreators.success(data)))
.catch(error => dispatch(actionCreators.failure(error)));
};
Then there's the reducer, of course in reducer.js:
export const initialState = {
loadingData: false,
data: [],
error: null,
};
const actionsMap = {
[actionTypes.RETRIEVE_REQUEST]: state => ({
...state,
loadingData: true,
}),
[actionTypes.RETRIEVE_SUCCESS]: (state, action) => ({
...state,
loadingData: false,
data: action.payload,
}),
[actionTypes.RETRIEVE_FAILURE]: (state, action) => ({
...state,
loadingData: false,
error: action.payload,
}),
};
export default (state = initialState, action) => {
const actionHandler = actionsMap[action.type];
if (!actionHandler) {
return state;
}
return actionHandler(state, action);
};
Data is then accessed via a selector:
const data = useSelector(state => state.data.data);
When running the code above, I am greeted with the following lovely error message:
TypeError: undefined is not a function (near '...}).then(function (response)...')
And in the emulator, I get pointed in the direction of these lines of code:
export const getData = (tk, values) => values.map((value) => apiInstance
.request({
url: ENDPOINTS.CATEGORIES,
method: 'POST',
data: qs.stringify({
token: tk,
category: value,
}),
}))
More specifically, the emulator seems to think that the error has to do with value.map, as it points a little red arrow at "values" just before the method.
Any idea on what went wrong?
Note
Upon refresh the error might change, for example just now it has shown the same error message but it points in the direction of
export const retrieveData = (tk, values) => dispatch => {
dispatch(actionCreators.request());
Promise.all(getData(tk, values))
.then(data => dispatch(actionCreators.success(data)))
.catch(error => dispatch(actionCreators.failure(error)));
};
More specifically, the little red arrow points at getData.
Refreshing again, and the error points at
useEffect(() => {
dispatch(retrieveData(tk, values));
}, [dispatch, value]);
Refrsh once more and it just loses it and goes for a module, as shown in the image:
It doesn't go further from there. Just mind that every single time, the error message is TypeError: undefined is not a function (near '...}).then(function (response)...'), it just points in a new direction.
Solved in
Unable to perform .map whithin function
Try to use a Promise.all():
export const getData = (tk, values) => values.map((value) => apiInstance
.request({
url: ENDPOINTS.CATEGORIES,
method: 'POST',
data: qs.stringify({
token: tk,
category: value,
}),
}))
Promise.all(getData(tk,values))
.then(responseValues => {
// Dispatch the response, it will come an array of values response.
})
.catch(eer => {
// Error handling
})
Read more about Promise.all() here

React-Query and Query Invalidation Question

I don't really know how to ask clearly but, I will paste my code first and ask below.
function useToDos() {
const queryCache = useQueryCache();
const fetchTodos = useQuery(
'fetchTodos',
() => client.get(paths.todos).then(({ data }: any) => data),
{ enabled: false }
);
const createTodo = async ({ name ) =>
await client.post(paths.todos, { name }).then(({ data }) => data);
return {
fetchTodos,
createTodo: useMutation(createTodo, {
onMutate: newItem => {
queryCache.cancelQueries('fetchTodos');
const previousTodos = queryCache.getQueryData('fetchTodos');
queryCache.setQueryData('fetchTodos', old => [
...old,
newItem,
]);
return () => queryCache.setQueryData('fetchTodos', previousTodos);
},
}),
};
}
As you can see, I am trying to create my own custom hooks that wrap react-query functionality. Because of this, I need to set my fetchTodos query to be disabled so it doesn't run right away. However, does this break all background data fetching?
Specifically, when I run createTodo and the onMutate method triggers, I would ideally like to have the fetchTodos query update in the background so that my list of todos on the frontend is updated without having to make the request again. But it seems that with the query initially set to be disabled, the background updating doesn't take effect.
As I don't think wrapping react-query hooks into a library of custom hooks is a very great idea, I will probably have more questions about this same setup but for now, I will start here. Thank you. 😊
The mutation does not automatically triggers a refetch. The way to achieve this using react-query is via queryCache.invalidateQueries to invalidate the cache after the mutation. From the docs:
The invalidateQueries method can be used to invalidate and refetch single or multiple queries in the cache based on their query keys or any other functionally accessible property/state of the query. By default, all matching queries are immediately marked as stale and active queries are refetched in the background.
So you can configure the useMutation to invalidate the query when the mutation settles. Example:
function useToDos() {
const queryCache = useQueryCache();
const fetchTodos = useQuery(
'fetchTodos',
() => client.get(paths.todos).then(({ data }: any) => data),
{ enabled: false }
);
const createTodo = async ({ name ) =>
await client.post(paths.todos, { name }).then(({ data }) => data);
return {
fetchTodos,
createTodo: useMutation(createTodo, {
onMutate: newItem => {
queryCache.cancelQueries('fetchTodos');
const previousTodos = queryCache.getQueryData('fetchTodos');
queryCache.setQueryData('fetchTodos', old => [
...old,
newItem,
]);
return () => queryCache.setQueryData('fetchTodos', previousTodos);
},
onSettled: () => {
cache.invalidateQueries('fetchTodos');
}
}),
};
}
What about splitting the logic into two different hooks? Instead of a monolith like useToDos?
That way you could have a hook for fetching:
const fetchData = _ => client.get(paths.todos).then(({ data }: any) => data)
export default function useFetchTodo(
config = {
refetchOnWindowFocus: false,
enabled: false
}
) {
return useQuery('fetchData', fetchData, config)
}
And in your mutation hook you can refetch manually, before createTodo
import useFetchTodo from './useFetchTodo'
//
const { refetch } = useFetchTodo()
// before createTodo
refetch()

Resources