manipulating SWR data with local data in react - javascript

I have an API with SWR that returns data something like this:
const {data: data, error: error} = useSWR('fetchData', fetcher, { refreshInterval: 5000 })
I want to display the status of my data data.status which is easy:
<p>{data.status}</P>
<button onClick={changeStatus}>change status</button>
but there are sometimes that I need to manually change what needs to be displayed, for that I tried to store data.status to a variable first and then change it using two things:
if (data){
status = data.status
}
const changeStatus = () => {
status = 'bad'
}
which simply doesn't change status AND
const [status, setStatus] = useState('');
if (data){
setStatus(data.status)
}
const changeStatus = () => {
setStatus('bad')
}
which gives me "too many iterations" error!
So how can I manually change the data I get from SWR in a way that it still calls the api every 5 seconds and update the data accordingly?

you cannot set it like that. you need to use useEffect if you want to store status in a state
useEffect(() => {
if (data) {
setStatus(data.status);
}
}, [data])

Related

React Prop returning Null as it relies on state

Hopefully a simply one.
I make an API call in my component which brings down some account information such as AccountUid, Category etc, i use state to set these.
useEffect(() => {
fetch(feed_url, {
headers: {
//Headers for avoiding CORS Error and Auth Token in a secure payload
"Access-Control-Allow-Origin": "*",
Authorization: process.env.REACT_APP_AUTH_TOKEN,
},
})
//Return JSON if the Response is recieved
.then((response) => {
if (response.ok) {
return response.json();
}
throw response;
})
//Set the Account Name state to the JSON data recieved
.then((accountDetails) => {
setAccountDetails(accountDetails);
console.log(accountDetails.accounts[0].accountUid);
console.log(accountDetails.accounts[0].defaultCategory);
})
//Log and Error Message if there is an issue in the Request
.catch((error) => {
console.error("Error fetching Transaction data: ", error);
});
}, [feed_url]);
This Works perfectly well and it Logs the correct values in my .then when testing it.
The issue however is that i want to pass these down as props. But i get an error that they are being returned as null (My default state).. i presume as they're jumping ahead.
<div className="App">
<GetAccountName
accountUID={accountDetails.accounts[0].accountUID}
defCategory={accountDetails.accounts[0].defaultCategory}
/>
</div>
How do i pass the the 2 details im logging as props?? I've tried setting default state to "" instead of null and just get that it is undefined.
If you dont want to use conditional render in your child component, so you should try optional chaining
<GetAccountName
accountUID={accountDetails?.accounts?.[0]?.accountUID}
defCategory={accountDetails?.accounts?.[0]?.defaultCategory}
/>
Since fetching is asyncronous, the most common way is to show some loading indicator (like a spinner) & once the data come in, show the component instead.
If you don't need an indicator, you might just return null.
The general idea is to manipulate some intermediary states (e.g. data, isError) based on the promise state.
Check out react-query library example or a lighter abstraction like useFetch hook to see how they manage it.
Here's a sample implementation of useFetch taken from this article:
const useFetch = (url, options) => {
const [response, setResponse] = React.useState(null);
const [error, setError] = React.useState(null);
const [abort, setAbort] = React.useState(() => {});
React.useEffect(() => {
const fetchData = async () => {
try {
const abortController = new AbortController();
const signal = abortController.signal;
setAbort(abortController.abort);
const res = await fetch(url, {...options, signal});
const json = await res.json();
setResponse(json);
} catch (error) {
setError(error);
}
};
fetchData();
return () => {
abort();
}
}, []);
return { response, error, abort };
};

setQueryData not updating the cache with React-Query

I have a very basic app where I'm trying to fetch some data, and update the cache. For example purposes I tried to update the data to an empty array, but on the dev tools and the console logs I keep getting the old data
function App() {
const queryClient = new QueryClient();
const { isLoading, error, data } = useQuery('repoData', fetcher, {
onSuccess: (data) => {
queryClient.setQueryData('repoData', () => []);
},
});
console.log('data', data);
return (
<div className="App">
<Home />
</div>
);
}
what would be the correct way to update the cache?
Why would you want to update the cache of the same item you have just successfully fetched? React-Query will put the result of the fetcher into the data field returned from useQuery - you don’t need to do anything in onSuccess for that
That's is an example from official documentation.
const queryClient = useQueryClient()
const mutation = useMutation(editTodo, {
onSuccess: data => {
queryClient.setQueryData(['todo', { id: 5 }], data)
}
})
mutation.mutate({
id: 5,
name: 'Do the laundry',
})
// The query below will be updated with the response from the
// successful mutation
const { status, data, error } = useQuery(['todo', { id: 5 }], fetchTodoById)
https://react-query.tanstack.com/guides/updates-from-mutation-responses
The data resolved from your fetcher function will populate the cache from react-query with your chosen query key.
This data is available when destructuring the useQuery hook or is available with the onSuccess callback.
It can be usefull to manually update the data as shown here:
https://stackoverflow.com/a/68949327/1934484
// fetcher function
function getProducts() {
// http call
const { data } = await http.get<{ products: ProductT[] }>(/products);
return data.products;
}
// data returned will be an array of products
const { data } = useQuery('products', getProducts, {
onSuccess: (data) => {
// data returned will be an array of products
},
});

react-query: don't refetch on an interval if there is an error

I'm refetching an API on an interval, but I want to stop it if there is an error.
const { status, data, error, isFetching } = useQuery(
'todos',
// only refetch if there is no error
async () => {
const res = await axios.get('/api/data')
return res.data
},
{
refetchInterval: intervalMs,
}
)
I could probably store the status.isError on a state variable and then use that with enabled but it seems a bit clunky. I couldn't find anything helpful going through the documentation.
The best way is indeed to do it via local state, but you don't need to disable the query if you don't want to - just setting the refetchInterval back to 0 might also be enough:
const [refetchInterval, setRefetchInterval] = useState(intervalMs)
useQuery(
key,
queryFn,
{
refetchInterval,
onError: () => setRefetchInterval(0)
}
)

Why is my custom hook called so many times?

I'm trying to implement a custom hook to provide the app with a guest shopping cart. My hook wraps around the useMutation hook from Apollo and it saves the shopping cart id in a cookie while also providing a function to "reset" the cart (basically, to remove the cookie when the order is placed).
Code time! (some code omitted for brevity):
export const useGuestCart = () => {
let cartId;
const [createCart, { data, error, loading }] = useMutation(MUTATION_CREATE_CART);
console.log(`Hook!`);
if (!cartId || cartId.length === 0) {
createCart();
}
if (loading) {
console.log(`Still loading`);
}
if (data) {
console.log(`Got cart id ${data.createEmptyCart}`);
cartId = data.createEmptyCart;
}
const resetGuestCart = useCallback(() => {
// function body here
});
return [cartId, resetGuestCart];
};
In my component I just get the id of the cart using let [cartId, resetCart] = useGuestCart(); .
When I run my unit test (using the Apollo to provide a mock mutation) I see the hooked invoked several times, with an output that looks something like this:
console.log src/utils/hooks.js:53
Hook!
console.log src/utils/hooks.js:53
Hook!
console.log src/utils/hooks.js:59
Still loading
console.log src/utils/hooks.js:53
Hook!
console.log src/utils/hooks.js:62
Got cart id guest123
console.log src/utils/hooks.js:53
Hook!
console.log src/utils/hooks.js:53
Hook!
I'm only getting started with hooks, so I'm still having trouble grasping the way they work. Why so many invocations of the hook?
Thank you for your replies!
Think of hooks as having that same code directly in the component. This means that every time the component renders the hook will run.
For example you define:
let cartId;
// ...
if (!cartId || cartId.length === 0) {
createCart();
}
The content inside the statement will run on every render as cartId is created every time and it doesn't have any value assigned at that point. Instead of using if statements use useEffect:
export const useGuestCart = () => {
const [cartId, setCartId] = useState(0);
const [createCart, { data, error, loading }] = useMutation(
MUTATION_CREATE_CART
);
const resetGuestCart = () => {
// function body here
};
useEffect(() => {
if(!cartId || cartId.length === 0){
createCart();
}
}, [cartId]);
useEffect(() => {
// Here we need to consider the first render.
if (loading) {
console.log(`Started loading`);
} else {
console.log(`Finished loading`);
}
}, [loading]);
useEffect(() => {
// Here we need to consider the first render.
console.log(`Got cart id ${data.createEmptyCart}`);
setCartId(data.createEmptyCart);
}, [data]);
return [cartId, resetGuestCart];
};
Also notice that there is no actual benefit from using useCallback if the component which is receiving the function is not memoized.

Store data from useQuery with useState

I'm using React hooks both to fetch GraphQL data with react-apollo and to store local state:
const [userData, setUserData] = useState({})
const { loading, error, data } = useQuery(USER_QUERY)
However, I'm wondering how to store data to userData. Is this how it's supposed to work:
useEffect(() => {
setUserData(data)
}, [Object.entries(data).length])
Looks like what you have probably works. There is also a onCompleted option available in the options parameter. it takes a callback of type:
(data: TData | {}) => void
so this is another way of doing it:
const { loading, error, data } = useQuery(USER_QUERY, {onCompleted: setUserData})
What are you trying to do with the returned data that you are unable to accomplish by simply using it as destructured from the query hook? In most use cases it can be used immediately, as it will update itself when refetched.
If it is necessary (and it could be), as the other answer says, the useEffect hook you posted should work, but I would replace the dependency with simply data, to prevent an edge case where the response has an equal length consisting of different data and does not update:
useEffect(() => {
setUserData(data)
}, [data])
I think something like this would work - you will need to create the initial state with useState, could be empty array and then onComplete in the useQuery would setTranscationsData... it is triggered every render when state or props change. Could of course add an inital state inside useState which insn't an empty array.
const [transactionsData, setTransactionsData] = React.useState([]);
const { error, data } = useQuery(GET_TRANSACTIONS, {
onCompleted: () => {
setTransactionsData(data.transactions);
},
});
another example
const [getLegalStatement] = useLazyQuery(GET_LEGAL_STATEMENT, {
fetchPolicy: 'network-only',
onCompleted: (data) => {
setTempLegalStatement(data.getLegalStatement);
},
onError: () => {
setTempLegalStatement({
consentedLegalStatementHash: '',
consentedSuppliersHash: '',
statement: '',
suppliersModal: '',
});
setTimeout(() => {
setRefetchNeeded(true);
}, 10000);
},
});
Use onSuccess
const [userData, setUserData] = useState({})
const { data, isLoading, error } = useQuery('QueryKey', QueryFunction, { onSuccess: setUserData })
This onSuccess callback function will fire setUserData(data) for you automatically any time the query successfully fetches new data.
To elaborate above, you can't use onSuccess/onSettled because those will not rerun if the data is cached, so if you leave the component and come back before the query expires your data won't get set.

Categories

Resources