How to mutate with query params in SWR hook? - reactjs

currently i am using SWR to data fetching, i trying to use Mutation feature of SWR to refetch the new data but this occurred a problem when i am calling mutate() by key was added new query params.
Here's my code not working:
import useSWR, { useSWRConfig } from 'swr'
function Profile () {
const { mutate } = useSWRConfig()
const { data } = useSWR('/api/post', fetcher)
return (
<div>
<h1>Title post {data.title}.</h1>
<button onClick={() => {
mutate('/api/post?author=1&pricing=1')
}}>
View more information of this post!
</button>
</div>
)
}
I read docs from SWR and i know the key of the mutate should be the same to key in useSWR() but in my case need more query params to get the corresponding data
How can i solve this problem?
Helps me please!

I wouldn't recommend using mutate in this scenario because the key (the URL) you want to use in the mutation is different from the original one. When mutate is called it'll update the cache for '/api/post' which will then contain data from '/api/post?author=1&pricing=1' in it.
As an alternative, I'd suggest you make the key an array in the useSWR call so that multiple arguments can be passed to the fetcher.
const [queryParams, setQueryParams] = useState('')
const { data } = useSWR(['/api/post', queryParams], fetcher)
Then, in your button onClick handler, you can update the queryParams state value to trigger a re-render and initiate a new request with the query parameters.
<button onClick={() => {
setQueryParams('?author=1&pricing=1')
}}>
View more information of this post!
</button>
You also need to modify the fetcher function slightly to expect multiple arguments, and also append the query params you pass to the URL.
const fetcher = (url, queryParams = '') => {
// Example fetch to demonstrate the logic
return fetch(`${url}${queryParams}`)
}
With this change you now have different keys (and cached data) for each URL you make a request for.

Related

Redux Toolkit RTK Query remove cache

I have an endpoint that generates and return a url for making payment online, it accepts only two arguments amount and currency.
initializeCardDeposit: builder.query({
query: ({ amount, currency }) => ({
url: "/initialize/card/deposit",
method: "POST",
body: {
amount,
currency,
},
}),
transformResponse: ({ data }) => data,
providesTags: ["cardDepositUrl"],
})
I want to get a new url anytime the the query is called with the same amount and currency, instead it returns a cached data.
I have tried dispatching invalidateTags("cardDepositUrl") and it didn't work.
1 -> First of all, you have to provide tagTypes to your service
2 -> You provideTags should have only the values mentioned inside tagTypes
3 -> invalidateTags[], should be called from another endpoint of the same service
Checkout this documentation -> https://redux-toolkit.js.org/rtk-query/usage/automated-refetching
or
Re-fetching on demand with refetch/initiate
In order to achieve complete granular control over re-fetching data, you can use the refetch function returned as a result property from a useQuery or useQuerySubscription hook.
Calling the refetch function will force refetch the associated query.
Alternatively, you can dispatch the initiate thunk action for an endpoint, passing the option forceRefetch: true to the thunk action creator for the same effect.
Force refetch example
import { useDispatch } from 'react-redux'
import { useGetPostsQuery } from './api'
const Component = () => {
const dispatch = useDispatch()
const { data, refetch } = useGetPostsQuery({ count: 5 })
function handleRefetchOne() {
// force re-fetches the data
refetch()
}
function handleRefetchTwo() {
// has the same effect as `refetch` for the associated query
dispatch(
api.endpoints.getPosts.initiate(
{ count: 5 },
{ subscribe: false, forceRefetch: true }
)
)
}
return (
<div>
<button onClick={handleRefetchOne}>Force re-fetch 1</button>
<button onClick={handleRefetchTwo}>Force re-fetch 2</button>
</div>
)
}
reference -> https://redux-toolkit.js.org/rtk-query/usage/cache-behavior#re-fetching-on-demand-with-refetchinitiate
You can use useLazyQuery instead of useQuery in your component and make use of trigger function returned by useLazyQuery hook. Trigger function always initiates a new request, even if cached result is available.
Inside your component
const [trigger,{data}]= useLazyInitializeCardDepositQuery();
When you want to fetch data, initiate request using trigger({amount,currency})
If you want to fetch data on initial render, you can add an useEffect hook
useEffect(()=>{
trigger({amount,currency})
},[])
uselazyquery documentation: https://redux-toolkit.js.org/rtk-query/api/created-api/hooks#uselazyquery

Where should you store incoming data from API in react?

I was wondering what the best practice is for storing/using data from an API in a react app. Here's a hypothetical example:
A simple app that fetches blog posts from an API and a control bar above the posts that can filter the posts by date, category, etc.
Which approach would be better:
1). Fetch every single post with useEffect, for example, and then store the returned data in state, and then create the filtering functions which filter the data that is stored in state.
2). Fetch every single post on first render, again with useEffect, and then create filtering functions which make multiple requests to the API for the required data.
Something like this
const BlogPage = () => {
//state where store posts (if u don't use redux)
const [data, setData] = useState([])
//request with or without params
const fetchData = async (queryParams = '') => {
// res = await request....
setData(res)
}
useEffect(() => {
fetchData()
}, [])
return <>
<Filters onSubmit={fetchData}/>
<Table data={data}/>
</>
}
You can update this with loaders, empty data render templates, adding redux or other
If you don't have an API with query params and you have no server pagination you must write filter function and replace <Filters onSubmit={fetchData}/> to <Filters onSubmit={filter}/>. The result of this function must call setData.

How to update React state once useMutation is done?

I add an user to an api with react-query's useMutation hook. It works. Now, I need to add the new user to my array of users, which is in my state.
I know I'm supposed to query all the users with useQuery and then use onSuccess inside useMutation to modify the cache. But in certain cases, I don't fetch the users with useQuery, so I need to update a local state as I would do with a normal promise.
For the moment, I simply check if the prop "success" is true and if so, I update the array. But it only works on the second click. Why and how to fix this?
It seems the success condition inside onAddUser() is only reached on a second click.
export default function App() {
const { updateUser } = userService();
const { update, loading, error, data, success } = updateUser();
const [users, setUsers] = useState(allUsers);
const onAddUser = async () => {
await update(newUser);
if (success) {
return setUsers((users) => [...users, data]);
}
};
return (
<>
<div>
{users.map((user) => (
<div key={user.id}>
{user.name} - {user.job}
</div>
))}
</div>
{loading && <div>sending...</div>}
{error && <div>error</div>}
<button onClick={() => onAddUser()}>add user</button>
</>
);
}
Here is also a sandbox: https://codesandbox.io/s/usemutation-test-co7e9?file=/src/App.tsx:283-995
The success prop returned from updateUser (I assume this is somehow the result returned by useMutation) will only update on the next render cycle. Functions will still keep the reference to whatever they closured over, even if you have an async await in there. This is nothing react-query specific, this is just how react works.
I would suggest to use the onSuccess callback of the mutation function, or use mutateAsync and check the result, though you have to keep in mind to catch errors manually if you use mutateAsync. You can read about mutateAsync here, I'm gonna show you a mutate example:
const { mutate, loading } = useMutation(() => ...);
const onAddUser = () =>
mutate(newUser, {
onSuccess: (newData) => setUsers((users) => [...users, data]);
});
};
also, please don't violate the rules of hooks. You can only call hooks from functional components or other hooks (=functions that start with use), but in your codesandbox, you call useMutation from the updateUser function ...

What is the best way to mutate remote data fetched with useQuery

I am fairly new to graphQL and Apollo. I hope I can make myself clear:
I am fetching data using the apollo/react-hook useQuery. Afterwards I populate a form with the data so the client can change it. When he is done, the data gets send back to the server using useMutation.
Until now, I use onCompleted to store the fetched data in the component state. That looks like this:
import React, { useState } from 'react';
import { TextField } from '#material-ui/core';
const Index = () => {
const [state, setState] = useState(null)
const {data, loading, error} = useQuery<typeof queryType>(query, {
onCompleted: data => {
// modify data slightly
setState(data)
}
})
return (
<TextField value={state} onChange={() => setState(event.target.value)}/>
)
}
The form than uses the values stored in the component state and the form handlers use setState
to change it.
My question now is, if this is the best practice and if the storing of the fetched data in a local component state neccessary.
Because you don't want to just fetch and render the data -- you want to be able to mutate it when the form values change -- we can't just utilize the data as is. Unless you're using uncontrolled inputs and refs to manage your forms (which you probably shouldn't do), then you're going to need to use some component state.
I think your current approach is mostly fine. The biggest downside is that if the query takes a while (maybe the user has a spotty internet connection), there's a window for them to start filling out the form only to have their input overridden once the query completes. You'd have to explicitly disable the inputs or hide the form until loading completes in order to prevent this scenario.
An alternative approach is to split your component into two:
const Outer = () => {
const {data, loading, error} = useQuery(query)
if (!data) {
return null // or a loading indicator, etc.
}
return <Inner data={data}/>
}
const Inner = ({ data }) => {
const [value, setValue] = useState(data.someField)
<TextField value={value} onChange={() => setValue(event.target.value)}/>
}
By not rendering of the inner component until loading is complete, we ensure that the initial props we pass to it have the data from the query. We can then use those props to initialize our state.
Seems like useQuery only has a state for responseId: https://github.com/trojanowski/react-apollo-hooks/blob/master/src/useQuery.ts
But you get a refetch function from the useQuery as well.
const { loading, error, data, refetch } = useQuery(...);
Try calling refetch(), after you used useMutation() to update the data. It shouldn't trigger a rerender. You probably have to set your own state for that reason.
Maybe something like:
const handleUpdate = () =>{
setData(refetch());
}
Alternativly you get the data after using useMutation which is also in the state: https://github.com/trojanowski/react-apollo-hooks/blob/master/src/useMutation.ts
const [update, { data }] = useMutation(UPDATE_DATA);
data will always be the newest value so you could also do:
useEffect(()=>{
setData(data);
// OR
// setData(refetch());
}, [data])

Apollo and React: refreshQueries not working in mutation fired from onClick()

I'm building a simple shopping app. On load, we'll make a query to check whether a cart exists. For now, that query always returns null (I haven't implemented logic yet to check whether a user has a shopping cart already). Then, when a user clicks "create cart", we do a mutation to create it. Finally, we'll use refreshQueries after the mutation finishes to fetch the cart (and its products) by ID, which is returned from the mutation. We then render those products in a pure component.
The problem I'm encountering is that nothing re-renders after users click the button and refreshQueries happens. I know the query is being sent and returning a created cart with products by looking at my developer tools' network tab. It's just that Apollo doesn't seem to notice the change.
Mongo is my back-end.
Here's the relevant code:
// query
import { gql } from 'react-apollo';
export const cartQuery = gql`
query CartQuery($cartId: ID) {
cart(cartId: $cartId) {
_id,
products {
_id,
name
}
}
}
`;
// mutation
import { gql } from 'react-apollo';
export const createCartMutation = gql`
mutation CreateCartMutation {
createCart {
_id
}
}
`;
// Apollo + React stuff
import React from 'react';
import { graphql } from 'react-apollo';
import { createCartMutation } from '../../mutations';
import { cartQuery } from '../../queries';
const BuildCart = ({ mutate }) => (
<button
onClick={() => {
// even when I hardcode the ID of a cart that exists and contains products, it doesn't matter
mutate({
refetchQueries: [{ query: cartQuery, variables: { cartId: '12345abcd' } }],
});
}}
>
Click
</button>
);
const BuildCartConnected = graphql(createCartMutation)(BuildCart);
const Cart = ({ data, data: { cart, loading } }) => {
console.log(cart); // always null, even after clicking button
return (
<div>
<BuildCartConnected />
{loading && <p>Loading...</p>}
// never gets to this step, even though the response is a properly formed cart with an array of products
{cart && cart.products.map(product => (
<p key={Math.random()}>{product.name}</p>
))}
</div>
);
};
const CartConnected = graphql(cartQuery)(Cart);
export default CartConnected;
If it's helpful, here's what the response from refetchQueries looks like in the network tab:
{"data":{"cart":{"_id":"12345abcd","products":[{"_id":"abcdef12345","name":"Product A","__typename":"Product"}],"__typename":"Cart"}}}
Your cart query takes a cartId variable. In the code above, you're not providing that variable, so its value is undefined. Apollo associates that combination of query plus variable(s) with your Cart component.
When you call refetch queries with a different variable than what is provided to the HOC, the results of that new query are fetched and persisted in the store. However, as far as Apollo knows, you still want the results of a query with undefined as the variable value for that component, not this new result you had it fetch.
refetchQueries shouldn't be utilized for what you're trying to do. Instead, the variable for the cart query should be derived from props:
const options = ({cartId}) => ({variables: {cartId}})
const CartConnected = graphql(cartQuery, {options})(Cart)
Then, when you call mutate, it returns a Promise that will resolve to the data returned by the mutation. We can grab the cartId from the response and store it within application state (by calling setState, firing off a Redux action, etc.). You can then pass that state down to your Cart component as a prop. When the state is updated, the prop changes and the query with the newly provided variable is fetched.

Resources