How to implement caching in react-firebase-hooks? - javascript

I have been trying to implement caching in react-firebase-hooks but getting this error
React Hook "useCollectionDataOnce" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function
I know that I am calling it conditionally but is there any other way to do this? By the way, I am following this to implement caching.
const [randomQuestions, setRandomQuestions] = React.useState([]);
const cachedData = localStorage.getItem("randomQuestions");
React.useEffect(() => {
if (cachedData) {
setRandomQuestions(JSON.parse(cachedData));
} else {
let [values, loading, error] = useCollectionDataOnce(
db
.collection("questions")
.where("tags", "array-contains-any", tags.length ? tags : [""])
.limit(10),
{ idField: "id" }
);
}
if (values) {
localStorage.setItem("randomQuestions", JSON.stringify(values));
setRandomQuestions(values);
}
});
In the code, I am trying to check if cachedData already exists or not. If not, then query and fetch the data. If yes, then set randomQuestions.

React Hooks cannot be inside a conditional as stated here. You should use it outside the if/else statement.
Looking the documentation of useCollectionDataOnce I've found that the variables of the function are optional, so you can define an auxiliar variable that is populated in the if/else statment depending on your needs and it is passed to the useCollectionDataOnce.
const [randomQuestions, setRandomQuestions] = React.useState([]);
const cachedData = localStorage.getItem("randomQuestions");
React.useEffect(() => {
var query = "";
if (cachedData) {
setRandomQuestions(JSON.parse(cachedData));
query = "";
} else {
query = db
.collection("questions")
.where("tags", "array-contains-any", tags.length ? tags : [""])
.limit(10),
{ idField: "id" };
}
let [values, loading, error] = useCollectionDataOnce(query);
if (values) {
localStorage.setItem("randomQuestions", JSON.stringify(values));
setRandomQuestions(values);
}
});

Related

vuejs not able to use data from useMutation on template => async problem?

I'm using a mutation from gql with vue apollo in the front tom get some data, there's a number and an Email.
Once mutation is finished, I assign the value to refs:
const mobile = ref('')
const email = ref('')
// mutation logic:
const {
mutate: getContactInfo,
onDone,
onError
} = useMutation(
gql`
mutation ContactInfo($userId: String!) {
contactInfo(userId: $userId) {
mobile
email
}
}
`,
() => ({
variables: {
userId: props.userID
}
})
)
onDone(data => {
const res = data
mobile.value = res.data.contactInfo.mobile
email.value = res.data.contactInfo.email
})
down this code I got these next 2 functions:
const emailContact = async () => {
loading.value = true
await getContactInfo()
location.replace(`mailto:${email.value}`)
loading.value = false
}
const smsContact = async () => {
loading.value = true
await getContactInfo()
location.href = `sms:${mobile.value.slice(1)}`
loading.value = false
}
Both work well except that the vanilla js method => location.replace(mailto:${email.value}) is causing me issues on mobile devices while I can do the same with a <a / HTML tag and have no problems.
So my problem is that once I want to send the email ref to the template so I can fire the mailto:' ' from an <a / tag the data is not there. Even when I make a console.log(email.value) => I just see a blank line.
I mean, I know the data is there and that's why I can use both previous methods, but why can't I use it on the template or log it. Seems like an asynchronous issue.
Of course, thanks in advance && any suggestion/help is greatly appreciated!

Using Apollo's useQuery to return a value, not render

Apologies as I'm fairly new to React and couldn't find the answer when searching but accept I might be using this wrong.
I have several pages that utilise useQuery from Apollo and when "loading" variable is false it can then process the data and render which is easy enough.
However, I want to create a new file called GetCustomerID.jsx and have an arrow function which, when supplied with a user ID, will run a gql query and return the customer ID value. Not render components, just return the value.
The trouble is that once this is called from within another page it always just returns "loading"
const myQuery = gql`
query GetCustomerID($id: ID!) {
user(id: $id) {
data {
id
attributes {
customer {
data {
id
}
}
}
}
}
}
`;
const GetCustomerID = () => {
const [userId, setUserID] = useState(localStorage.getItem("userid"));
const [custId, setCustId] = useState();
const { loading, error, data } = useQuery(myQuery, {
variables: {
id: userId
}
});
if(loading){
return "loading"
}
if (error) {
console.log(error);
return "error";
}
setCustId(data.attributes.customer.id);
console.log(custId);
return custId;
};
Call from elsewhere:
const [custId, setCustId] = useState();
let myVal = GetCustomerID();
setCustId(myVal)
Any help would be much appreciated and if I'm fundamentally misunderstanding useQuery for what I'm trying to achieve then apologies
try to define custom hook, like
useCustomerId
inside this hook you can call useQuery or useLazyQuery and return result of the query

React Dropzone - async function in onDrop callback doesn't update state

I'm trying to use react-dropzone onDrop with useCallback function to update the array of files dropped onto the area and trigger the upload. My code looks like this:
const [files, setFiles] = useState([]);
...
const onDrop = useCallback((acceptedFiles) => {
setFiles(acceptedFiles);
handleFileUpload();
}, []);
const {
getRootProps,
getInputProps,
isDragActive,
} = useDropzone({ onDrop });
handleFileUpload is an asynchronous function, which uses Fetch API. When I try to log the files array inside this method, it appears to be empty, even though I updated the state before running this function.
const handleFileUpload = async () => {
console.log(files); // <- returns empty array
}
I also tried to set different variables to indicate if the upload has started, etc. but changing the values within this method doesn't update the state of my component at all. Is it even possible to use an async function in a callback like this? Should I trigger the file upload somewhere else? I feel like I don't quite understand the concept here.
Your state will not update until after your code exits and/or relinquishes control. i.e. after you call setFiles() the files variable will still be STALE until the code returns to the event loop.
Just pass the accepted files into the function.
const onDrop = useCallback((acceptedFiles) => {
setFiles(acceptedFiles); // <-- that won't update right away
handleFileUpload(acceptedFiles);
}, []);
UPDATE
Here are snippets from one of my projects. I just stuff the dropped file into state, as you do. That causes the component to re-render, and when it does, I catch the updated file state in a hook, async parse the file, and return the data - which is then consumed by the component. The component shows UI states - importing if fileToImport is set and dataToImport is not yet available, etc...
// MyComponent.tsx
const MyComponent = () => {
const [fileToImport, setFileToImport] = useState<File | undefined>()
const [dataToImport, dataToImportError] = useReadFileData(fileToImport)
const onDrop = useCallback(acceptedFiles => {
setFileToImport(acceptedFiles[0])
}, [])
//useREadFileData.ts hook
import { useEffect, useState } from 'react'
const useReadFileData = (file: File | undefined): [any[], string | undefined] => {
const [rows, setRows] = useState<unknown[]>([])
const [error, setError] = useState<string>()
useEffect(() => {
async function parseFile(file: File) {
try {
// snip
setRows(rows)
} catch (error: any) {
if (typeof error === "object") {
setError(error?.message ?? "Error parsing file")
}
}
}
// only if we have a file to parse
if (file) {
parseFile(file)
}
}, [file, setError, setRows])
return [rows, error]
}
export default useReadFileData

UseQuery graphql -> useState with useEffect such that I can run second useEffect to set more state

I am using Apollo for graphQL queries in a NextJS project I have the following request. I feel like the solution to this is simple, but the error occurs at the stateData.allProducts section it is saying it is null, but I have set the state in the useEffect and it has data as a dependency in the array, so shouldn't it re-render once data has loaded.
This is all works just fine if I placed these checks below a if statement checking the loading and return ...loading but then I can't use useEffect.
Any help as to what I am doing wrong would be greatly appreciated.
Thanks!
const { data, loading, error } = useQuery(QUERY);
const Router = useRouter();
// Creating state and setting it from query
const [stateData, setStateData] = useState(null);
const [disableAddToCart, setDisableAddToCart] = useState(false);
useEffect(() => {
setStateData(data);
}, [data]);
//~~~~~~~// <--- RIGHT HERE IS WHERE I GET THE NULL ERROR
const productFound = stateData.allProduct.find(
(product: any) => product.slug.current === Router.query.product
);
const currentItem = cartDetails[productFound.id];
useEffect((): void => {
console.log("currentItem", currentItem);
if (currentItem) {
if (currentItem.quantity > 0) {
setDisableAddToCart(true);
} else {
setDisableAddToCart(false);
}
}
}, [currentItem]);
As long as your query is loading, or if there is an error, the data variable from useQuery will be null.
Therefore you have to check for the loading to have finished and that no error has occurred. And/or for the data to be defined.
Also, stateData is unnecessary, because data is already a ready-to-use state variable.
const { data, loading, error } = useQuery(QUERY);
const Router = useRouter();
const [disableAddToCart, setDisableAddToCart] = useState(false);
let productFound;
let currentItem;
if(!loading && !error && data?.allProduct){
productFound = data.allProduct.find(
(product: any) => product.slug.current === Router.query.product
);
currentItem = cartDetails[productFound.id];
}
useEffect((): void => {
//since you are using typescript, you can use the optional chaining operator
if (currentItem?.quantity > 0) {
setDisableAddToCart(true);
} else {
setDisableAddToCart(false);
}
}, [currentItem]);

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.

Categories

Resources