What i'm trying to do: skip the query if the parameter in the query doesn't change.
I understand that useQuery is smart enough to not run if the input parameter doesn't change. However, if I have useQuery in componentA, and i switch from it to componentB and back to componentA, useQuery will run again even if the input parameter is the same.
So, I thought of using the skip option in useQuery. However its behaviour is not as expected. If I define a constant that is a boolean outside and set it to the 'skip' option, the query isn't skipped even if my constant was 'true'.
const skipQuery = props.match.params.itineraryId == placeState.itineraryId;
console.log(skipQuery) // prints true
const { data } = useQuery(GET_ITINERARY, {
skip: skipQuery,
onCompleted(data){
fetchItineraryFromGoogle(data);
},
variables: {itineraryId}
})
However this works:
const { data } = useQuery(GET_ITINERARY, {
skip: true,
onCompleted(data){
fetchItineraryFromGoogle(data);
},
variables: {itineraryId}
})
so does this:
const { data } = useQuery(GET_ITINERARY, {
skip: 1==1,
onCompleted(data){
fetchItineraryFromGoogle(data);
},
variables: {itineraryId}
})
so why doesn't my boolean constant work?
Related
I am using react-redux and redux-toolkit for this project. I want to add item to the cart. If there is already a same item in cart, just increment the unit. The error occurs at the slice, so I will just show the slice here.
const CartListSlice = createSlice({
name: 'cartItem',
initialState,
reducers: {
addToCart: (state, action) => {
let alreadyExist = false;
// get a copy of it to avoid mutating the state
let copyState = current(state.cartItem).slice();
// loop throught the cart to check if that item is already exist in the cart
copyState.map(item => {
if (item.cartItem._id === action.payload._id) {
alreadyExist = true;
item.unit += 1 // <--- Error occurs here
}
})
// If the item is not exist in the cart, put it in the cart and along with its unit
if (alreadyExist === false) {
state.cartItem.push({
cartItem: action.payload,
unit: 1
});
}
},
}
});
I get a type error telling me that unit is read-only.
How can I update the "unit" variable so that it increments whenever it is supposed to.
In React Toolkit's createSlice, you can modify the state directly and even should do so. So don't create a copy, just modify it.
In fact, this error might in some way stem from making that copy with current.
See the "Writing Reducers with Immer" documentation page on this
Meanwhile, a suggestion:
const CartListSlice = createSlice({
name: 'cartItem',
initialState,
reducers: {
addToCart: (state, action) => {
const existingItem = state.find(item => item.cartItem._id === action.payload._id)
if (existingItem) {
item.unit += 1
} else {
state.push({
cartItem: action.payload,
unit: 1
});
}
},
}
});
You don't need line:
let copyState = current(state.cartItem).slice();
Instead of copyState, just use state.cartItem.map
As #phry said, you should mutate state directly, because redux-toolkit is using immerJS in the background which takes care of mutations.
I'm using Apollo to call a rest endpoint that takes variables from query string:
/api/GetUserContainers?showActive=true&showSold=true
I'm having trouble figuring out how to pass variables to the query, so it can then call the correct url. From looking at apollo-link-rest docs and a few issues I think I'm supposed to use pathBuilder but this is not documented and I haven't been able to get it working.
So far I've defined my query like this:
getUserContainersQuery: gql`
query RESTgetUserContainers($showActive: Boolean, $showSold: Boolean, $pathBuilder: any) {
containerHistory #rest(type: "ContainerHistoryResponse", pathBuilder: $pathBuilder) {
active #type(name: "UserContainer") {
...ContainerFragment
}
sold #type(name: "UserContainer") {
...ContainerFragment
}
}
}
${ContainerFragment}
`
and calling it in my component like this, which does not work:
import queryString from 'query-string'
// ...
const { data } = useQuery(getUserContainersQuery, {
variables: {
showActive: true,
showSold: false,
pathBuilder: () => `/api/GetUserContainers?${queryString.stringify(params)}`,
},
fetchPolicy: 'cache-and-network',
})
The only way I got this to work was by passing the fully constructed path to the query from the component:
// query definition
getUserContainersQuery: gql`
query RESTgetUserContainers($pathString: String) {
containerHistory #rest(type: "ContainerHistoryResponse", path: $pathString) { // <-- pass path here, instead of pathBuilder
// same response as above
}
}
`
// component
const params = {
showActive: true,
showSold: false,
}
const { data } = useQuery(getUserContainersQuery, {
variables: {
pathString: `/api/GetUserContainers?${queryString.stringify(params)}`,
},
fetchPolicy: 'cache-and-network',
})
These seems to me like a really hacky solution which I'd like to avoid.
What is the recommended way to handle this query string problem?
You shouldn't need to use the pathBuilder for simple query string params. You can pass your params directly as variables to useQuery then pass then directly into teh path using the {args.somearg} syntax. The issue I see is you've not defined the variables your using for you query containerHistory bu only in the query alias RESTgetUserQueries. If updated is should look like this:
// query definition
getUserContainersQuery: gql`
query RESTgetUserContainers($showActive: Boolean, $showSold: Boolean) {
// pass the variables to the query
containerHistory(showActive:$showActive, showSold:$showSold) #rest(type: "ContainerHistoryResponse", path:"/api/GetUserContainers?showActive={args.showActive}&showSold={args.showTrue}") {
//.. some expected reponse
}
}
`
// component
const params = {
showActive: true,
showSold: false,
}
const { data } = useQuery(getUserContainersQuery, {
variables: {
showActive,
showSold
},
fetchPolicy: 'cache-and-network',
})
I'm using useLazyQuery() to get analytic data measured by a time filter. When the time filter change, the cache returns the same values despite the filter.
I'm using the default fetchPolicy (cache-first).
const [
getVisitorsAnalytics,
{ loading, data },
] = useAnalyticVisitorsLazyQuery()
const handleTimeFilterChange = (timeFilter: string) => {
getVisitorsAnalytics({
variables: {
input: {
timeFilter,
},
},
})
}
Thanks and regards!
I think the values are cached and the cache doesn't have the data for the new filter value passed -
const { data, loading } = useLazyQuery(QUERY, {
fetchPolicy: 'network-only'
});
I'm trying to use React hooks for memoizing a callback. This callback specifically uses a function that's defined on an object.
const setValue = useCallback((value) => {
field.setValue(key, value);
}, [field.setValue, key]);
This triggers Eslint rule react-hooks/exhaustive-deps.
It wants me to instead pass in [field, key].
If I then change the code to the following, I get no warning even though it seems equivalent:
const { setValue: setFieldValue } = field;
const setValue = useCallback((value) => {
setFieldValue(key, value);
}, [setFieldValue, key]);
Why is Eslint warn me in the first example?
Can I safely ignore it in such circumstances?
Try this.
const setValue = useCallback((value) => {
const set = field.setValue;
set(key, value);
}, [field.setValue, key]);
But it's not recommended.Take a look at this explanation. https://github.com/facebook/react/issues/15924#issuecomment-521253636
I have a selector:
const mySelector = createSelector(
selectorA,
selectorB,
(a, b) => ({
field1: a.field1,
field2: b.field2
})
)
I know the selector is evaluated when any of its inputs change.
In my use case, I need to control "mySelector" by a third selector "controlSelector", in the way that:
if "controlSelector" is false, "mySelector" does not evaluate a new value even in the case "selectorA" and/or "selectorB" changes, and returns the memoized value
if "controlSelector" is true, "mySelector" behaves normally.
Any suggestions?
Selectors are pure functions..its will recalculate when the input arguments are changed.
For your case its better to have another state/object to store the previous iteration values.
You can pass that as selector and based on controlSelector value you can decide what you can return.
state : {
previousObj: {
...
}
}
const prevSelector = createSelector(
...,
(state) => state.previousObj
)
const controlSelector = createSelector(...);
const mySelector = createSelector(
controlSelector,
prevSelector,
selectorA,
selectorB,
(control, a, b) => {
if(control) {
return prevSelector.previousObj
} else {
return {
field1: a.field1,
field2: b.field2
};
}
}
)
Sorry for the delay...
I have finally solved the issue not using NGRX selectors to build up those "higher selectors" and creating a class with functions that use combineLatest, filter, map and starWith
getPendingTasks(): Observable<PendingTask[]> {
return combineLatest(
this.localStore$.select(fromUISelectors.getEnabled),
this.localStore$.select(fromUISelectors.getShowSchoolHeadMasterView),
this.memStore$.select(fromPendingTaskSelectors.getAll)).pipe(
filter(([enabled, shmView, tasks]) => enabled),
map(([enabled, shmView, tasks]) => {
console.log('getPendingTasks');
return tasks.filter(task => task.onlyForSchoolHeadMaster === shmView);
}),
startWith([])
);
}
Keeping the NGRX selectors simple and doing the heavy lifting (nothing of that in this example, though) in this kind of "selectors":
- will generate an initial default value (startWith)
- will not generate new value while filter condition fails (that is, when not enabled, any changes in the other observables do not fire a new value of this observable)