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()
Related
I have a useQuery which is disabled in a react function component. I have another useQuery that uses mutate and on the success it calls refetchMovies(). This all seems to work well but I'm seeing old data in the refetchMovies. Is there a way for to get the refetchMovies to always call fresh data from the server when its called ?
const MyComponent = () => {
const {data, refetch: refetchMovies} = useQuery('movies', fetchMovies, {
query: {
enabled: false
}
})
const {mutate} = useQuery('list', fetchList)
const addList = useCallback(
(data) => {
mutate(
{
data: {
collection: data,
},
},
{
onSuccess: () => refetchMovies(),
onError: (error) => console.log('error')
}
)
},
[mutate, refetchMovies]
)
return (
<div onClick={addList}> {data} </div>
)
}
Try to invalidate the query in your onSuccess callback instead of manually refetching it:
https://tanstack.com/query/v4/docs/react/guides/query-invalidation
Example:
// Invalidate every query with a key that starts with `todos`
queryClient.invalidateQueries({ queryKey: ['todos'] })
Fetching for the first time works, same goes for resetting the query. The data is fresh right after, then becomes stale after 5 seconds.
However, I want to refetch or invalidate the queries after applying mutations, but whenever I do so, the data just keeps refetching and never returning:
DevTools scr showing data just refetching
Same when i use the DevTools directly.
My useQuery hook:
export const useFetchCandidates = (id: string) => {
return useQuery<Entry<IKandidatFields>[], Error>(
'candidates',
async () => {
const res = await getCandidates(id)
return res
},
{
staleTime: 5000,
cacheTime: 10,
}
)
}
Using the hook to access data:
const {
data: candidates,
}: UseQueryResult<Entry<IKandidatFields>[], Error> = useFetchCandidates(id!)
The mutation:
const mutation = useMutation(
() =>
sendCandidateToNextStage(candidateID, utlysning).then(() =>
getCandidates(id!)
),
{
onMutate: () => {
queryClient.cancelQueries(['candidates', id])
},
onSettled: () => {
queryClient.resetQueries(['candidates', id])
},
}
)
This was solved by using a get axios request instead of utilizing the cms client directly... still don't know why the one works instead of the other when both of them return the same object.
When using the contentful API client:
export async function getCandidates(
id: string
): Promise<Entry<IKandidatFields>[]> {
const res = await client
.getEntries<IKandidatFields>({
content_type: 'kandidat',
})
return res
}
it was constantly fetching and never worked.
However, when using an axios request instead
export const getCandidatesFromAPI = async (id: string) => {
const { data } = await axios.get(
`https://cdn.contentful.com/spaces/${spaceID}/environments/${environmentId}/entries?access_token=${accessToken}&content_type=${contentTypeKandidat}`
)
return data
}
as the mutation function, everything worked perfectly.
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
I am currently trying to load my product data into redux, but so far I cant seem to pass the product information returned from firestore into the reducer.
Index.js -> load first 10 products from firestore soon after store was created.
store.dispatch(getAllProducts)
action/index.js
import shop from '../api/shop'
const receiveProducts = products => ({
type: types.RECEIVE_PRODUCTS
products
})
const getAllProducts = () => dispatch => {
shop.getProducts(products => {
dispatch(receiveProducts)
})
}
shop.js
import fetchProducts from './firebase/fetchProducts'
export default {
getProducts: (cb) => cb(fetchProducts())
}
fetchProducts.js
const fetchProducts = async() => {
const ProductList = await firebase_product.firestore()
.collection('store_products').limit(10)
ProductList.get().then((querySnapshot) => {
const tempDoc = querySnapshot.docs.map((doc) => {
return { id: doc.id, ...doc.data() }
})
}).catch(function (error) {
console.log('Error getting Documents: ', error)
})
}
In product reducers
const byId = (state={}, action) => {
case RECEIVE_PRODUCTS:
console.log(action); <- this should be products, but it is now promise due to aysnc function return?
}
I can get the documents with no issues (tempDocs gets the first 10 documents without any issue.) but I am not able to pass the data back into my redux. If I were creating normal react app, I would add a loading state when retrieving the documents from firestore, do I need to do something similar in redux as well ?
Sorry if the code seems messy at the moment.
fetchProducts is an async function so you need to wait for its result before calling dispatch. There are a few ways you could do this, you could give fetchProducts access to dispatch via a hook or passing dispatch to fetchProducts directly.
I don't quite understand the purpose of shop.js but you also could await fetchProducts and then pass the result of that into dispatch.
A generalized routine I use to accomplish exactly this:
const ListenGenerator = (sliceName, tableName, filterArray) => {
return () => {
//returns a listener function
try {
const unsubscribe = ListenCollectionGroupQuery(
tableName,
filterArray,
(listenResults) => {
store.dispatch(
genericReduxAction(sliceName, tableName, listenResults)
);
},
(err) => {
console.log(
err + ` ListenGenerator listener ${sliceName} ${tableName} err`
);
}
);
//The unsubscribe function to be returned includes clearing
// Redux entry
const unsubscriber = () => {
//effectively a closure
unsubscribe();
store.dispatch(genericReduxAction(sliceName, tableName, null));
};
return unsubscriber;
} catch (err) {
console.log(
`failed:ListenGenerator ${sliceName} ${tableName} err: ${err}`
);
}
};
};
The ListenCollectionGroupQuery does what it sounds like; it takes a tableName, an array of filter/.where() conditions, and data/err callbacks.
The genericReduxAction pretty much just concatenates the sliceName and TableName to create an action type (my reducers de-construct action types similarly). The point is you can put the dispatch into the datacallback.
Beyond this, you simply treat Redux as Redux - subscribe, get, etc just as if the data were completely local.
I am using React Apollo to get data from my server. When my page loads I am using useQuery to retrieve the data. This works fine. The problem is when I make a change to the search form, this updates the state which causes an unwanted re-render which calls the server again.
I want to call the server only when the page loads and when I click the search button.
useQuery:
const { loading: loadingLessons, error: lessonsError, data: lessons } = useQuery(
LESSON_BY_PARAMS,
{
variables: { findLessonsInput: searchParams },
},
);
When I change a form field it calls updateFormField which updates the redux state causing a re-render
<Autocomplete
options={categories}
getOptionLabel={(option: any) => option.label}
inputValue={form.category}
defaultValue={() => {
const value = categories.find(option => option.label === form.category);
return value;
}}
onChange={(event, value) => updateFormField('category', value?.label)}
renderInput={params => (
<TextField {...params} label="Category" variant="outlined" fullWidth />
)}
/>
I am using react hooks.
Have a look at the skip option which can be used to entirely skip the query. You can do something like this:
const [skip, setSkip] = React.useState(false)
const { loading, data } = useQuery(QUERY, { skip })
React.useEffect(() => {
// check whether data exists
if (!loading && !!data) {
setSkip(true)
}
}, [data, loading])
So, once data returned you simply set skip option to true. If you want to make a request you should handle onClick on the search button(simply setSkip(false)).
If you use pure apollo client you will loose some features from apollo. There is no need to move graphql call into redux action, you can use selectors in Redux to prevent components from reloading or making unnecessary calls to a server.
Selectors in Redux
Thanks for the suggestion #Vadim Sheremetov however I decided to move the api call to a redux-thunk function:
When the page loads I dispatch an action passing it search params and apollo client:
const client = useApolloClient();
useEffect(() => {
loadLessons(searchParams, client);
}, [location.search]);
const mapDispatchToProps = dispatch => {
return {
loadLessons: (searchParams, client) =>
dispatch(loadLessonsAction(searchParams, client)),
};
};
The action invokes a redux thunk function which then dispatches another action that updates the state:
actions.ts:
export function loadLessonsAction(searchParams: any, client: ApolloClient<object>):
any {
return async function(dispatch) {
try {
const { data } = await client.query({
query: LESSON_BY_PARAMS,
variables: { findLessonsInput: searchParams },
});
dispatch(loadLessonsSuccessAction(data.lessonsByParams));
} catch (error) {}
};
}
export function loadLessonsSuccessAction(lessons: any[]): FilterPanelActionTypes {
return {
type: LOAD_LESSONS_SUCCESS,
payload: lessons,
};
}
reducer.ts:
case LOAD_LESSONS_SUCCESS: {
return {
...state,
lessons: action.payload.lessons,
lessonCount: action.payload.count,
};
}