How to run useQuery inside forEach? - javascript

I have loop - forEach - which find productId for every element of array. I want to fetch my database by productId using apollo query.
How to do it?
products.forEach(({ productId, quantity }) =>
// fetch by 'productId'
);

From the rules of hooks:
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function. By following this rule, you ensure that Hooks are called in the same order each time a component renders.
Hooks cannot be used inside a loop, so you can't use them inside a forEach callback.
You should create a separate component for each product that only uses the useQuery hook once. You can then map over the products and return the component for each one:
const YourComponent = () => {
...
return products.map(({ productId, quantity }) => (
<Product key={productId} productId={productId} quantity={quantity} />
))
}
const Product = () => {
const { data, error, loading } = useQuery(...)
// render your data accordingly
}

To run useQuery based on logic or manually multiple times by setting refetchOnWindowFocus & enable properties to false and then calling method refetch(). example as below.
const RefetchExample = () => {
const manualFetch = {
refetchOnWindowFocus: false,
enabled: false
}
const GetAssetImage = async () => {
const {data} = await axiosInstance.get("Your API")
return data
}
const {assetImage, refetch} = useQuery('assetImage', GetAssetImage, manualFetch)
const imageFetch = () => {
refetch();
}
return (
<Button onClick={imageFetch}>Refetch</Button>
)
}
export default RefetchExample

If you want to perform multiple calls to useQuery then you can't do that in a forEach, map etc. You need to use useQueries e.g.
function Products({ productIds }) {
const productQueries = useQueries({
queries: productIds.map(productId => {
return {
queryKey: ['productById', productId],
queryFn: () => fetchProductById(productId),
}
})
})
Example taken from: https://tanstack.com/query/v4/docs/guides/parallel-queries

Related

Only one item is added in state when adding multiple with multiple setState calls

For learning purposes, I'm creating an e-shop, but I got stuck with localStorage, useEffect, and React context. Basically, I have a product catalog with a button for every item there that should add a product to the cart.
It also creates an object in localStorage with that item's id and amount, which you select when adding the product to the cart.
My context file:
import * as React from 'react';
const CartContext = React.createContext();
export const CartProvider = ({ children }) => {
const [cartProducts, setCartProducts] = React.useState([]);
const handleAddtoCart = React.useCallback((product) => {
setCartProducts([...cartProducts, product]);
localStorage.setItem('cartProductsObj', JSON.stringify([...cartProducts, product]));
}, [cartProducts]);
const cartContextValue = React.useMemo(() => ({
cartProducts,
addToCart: handleAddtoCart, // addToCart is added to the button which adds the product to the cart
}), [cartProducts, handleAddtoCart]);
return (
<CartContext.Provider value={cartContextValue}>{children}</CartContext.Provider>
);
};
export default CartContext;
When multiple products are added, then they're correctly displayed in localStorage. I tried to log the cartProducts in the console after adding multiple, but then only the most recent one is logged, even though there are multiple in localStorage.
My component where I'm facing the issue:
const CartProduct = () => {
const { cartProducts: cartProductsData } = React.useContext(CartContext);
const [cartProducts, setCartProducts] = React.useState([]);
React.useEffect(() => {
(async () => {
const productsObj = localStorage.getItem('cartProductsObj');
const retrievedProducts = JSON.parse(productsObj);
if (productsObj) {
Object.values(retrievedProducts).forEach(async (x) => {
const fetchedProduct = await ProductService.fetchProductById(x.id);
setCartProducts([...cartProducts, fetchedProduct]);
});
}
}
)();
}, []);
console.log('cartProducts', cartProducts);
return (
<>
<pre>
{JSON.stringify(cartProductsData, null, 4)}
</pre>
</>
);
};
export default CartProduct;
My service file with fetchProductById function:
const domain = 'http://localhost:8000';
const databaseCollection = 'api/products';
const relationsParams = 'joinBy=categoryId&joinBy=typeId';
const fetchProductById = async (id) => {
const response = await fetch(`${domain}/${databaseCollection}/${id}?${relationsParams}`);
const product = await response.json();
return product;
};
const ProductService = {
fetchProductById,
};
export default ProductService;
As of now I just want to see all the products that I added to the cart in the console, but I can only see the most recent one. Can anyone see my mistake? Or maybe there's something that I missed?
This looks bad:
Object.values(retrievedProducts).forEach(async (x) => {
const fetchedProduct = await ProductService.fetchProductById(x.id);
setCartProducts([...cartProducts, fetchedProduct]);
});
You run a loop, but cartProducts has the same value in every iteration
Either do this:
Object.values(retrievedProducts).forEach(async (x) => {
const fetchedProduct = await ProductService.fetchProductById(x.id);
setCartProducts(cartProducts => [...cartProducts, fetchedProduct]);
});
Or this:
const values = Promise.all(Object.values(retrievedProducts).map(x => ProductService.fetchProductById(x.id)));
setCartProducts(values)
The last is better because it makes less state updates
Print the cartProducts inside useEffect to see if you see all the data
useEffect(() => {
console.log('cartProducts', cartProducts);
}, [cartProducts]);
if this line its returning corrects values
const productsObj = localStorage.getItem('cartProductsObj');
then the wrong will be in the if conditional: replace with
(async () => {
const productsObj = localStorage.getItem('cartProductsObj');
const retrievedProducts = JSON.parse(productsObj);
if (productsObj) {
Object.values(retrievedProducts).forEach(async (x) => {
const fetched = await ProductService.fetchProductById(x.id);
setCartProducts(cartProducts => [...fetched, fetchedProduct]);
});
}
}
Issue
When you call a state setter multiple times in a loop for example like in your case, React uses what's called Automatic Batching, and hence only the last call of a given state setter called multiple times apply.
Solution
In your useEffect in CartProduct component, call setCartProducts giving it a function updater, like so:
setCartProducts(prevCartProducts => [...prevCartProducts, fetchedProduct]);
The function updater gets always the recent state even though React has not re-rendered. React documentation says:
If the new state is computed using the previous state, you can pass a function to setState. The function will receive the previous value, and return an updated value.

why wont jsx render firestore data

I'm trying to GET data from firestore and render it, in the most basic way possible. I can console.log the data, but as soon as I try to render it via JSX, it doesn't. Why is this?
import React from 'react'
import { useState, useEffect } from 'react'
import {db} from '../../public/init-firebase'
export default function Todo() {
const [todo, setTodo] = useState()
const myArray = []
//retrive data from firestore and push to empty array
function getData(){
db.collection('Todos')
.onSnapshot((querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log(doc.id +'pushed to myArray')
myArray.push(doc.id)
})
})
}
useEffect(() => {
getData()
}, [])
return (
<>
<div>
<h1>Data from firestore: </h1>
{myArray.map((doc) => {
<h1>{doc.id}</h1>
console.log('hi')
})}
</div>
</>
)
}
First, change myArray to State like this:
const [myArray, setMyArray] = useState([]);
Every change in myArray will re-render the component.
Then, to push items in the State do this:
function getData(){
db.collection('Todos')
.onSnapshot((querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log(doc.id +'pushed to myArray')
setMyArray(oldArray => [...oldArray, doc.id])
})
})
}
You're just pushing the ID in myArray, so when to show try like this:
{myArray.map((id) => {
console.log('hi')
return <h1 key={id}>{id}</h1>
})}
If you look closely,
{myArray.map((doc) => {
<h1>{doc.id}</h1>
console.log('hi')
})}
those are curly braces {}, not parenthesis (). This means that although doc exists, nothing is happening since you are just declaring <h1>{doc.id}</h1>. In order for it to render, you have to return something in the map function. Try this instead:
{myArray.map((doc) => {
console.log('hi')
return <h1>{doc.id}</h1>
})}
In order to force a "re-render" you will have to use the hooks that you defined
https://reactjs.org/docs/hooks-intro.html
function getData(){
db.collection('Todos')
.onSnapshot((querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log(doc.id +'pushed to myArray')
myArray.push(doc.id)
})
})
// in case the request doesn't fail, and the array filled up with correct value using correct keys:
// i'd setState here, which will cause a re-render
setTodo(myArray)
}

How can update my useState variable if I'm already using useEffeck hook?

I'm working in a personal project to learn more about the way react hooks works.
Recently I posted a question where I was using a variable inside an axios call and in the moment when I tried to update it (setVariable), it didn't worked. The answer I learn that useState make asynchonous calls so my variable didn't update so I can use useEffect to solve that.
But now I'm doing another axios call and I'm already using my useEffect hook in that component so I think it can't be used twice. Can you please explain me what can I do in those cases?
Here is the component that I'm working with:
type modalBodyFormProps = {
handleSubmit: any,
objectS: any
}
const ModalBodyUpdatePreparacion: React.FC = (props: modalBodyFormProps) => {
const[stockPreparaciones, setStockPreparaciones] = useState<any[]>([]);
const[ingsPreparaciones, setIngsPreparaciones] = useState<any[]>([]);
useEffect(() => {
getPreparaciones();
getIngredientePrep();
},[stockPreparaciones.length]);
const getPreparaciones = () => {
console.log(props.objectS);
axios.get('https://inventario-services.herokuapp.com/invservice/stock/getone/?codigo=' + props.objectS)
.then(result => {
console.log(result);
setStockPreparaciones(result.data.preparaciones); //Here is where I need to use useEffect hook so this value can be updated
console.log(stockPreparaciones);
}).catch(console.log);
}
const getIngredientePrep = () => {
stockPreparaciones.map(st => {
axios.get('https://inventario-services.herokuapp.com/invservice/stock/getone/?codigo=' + st.codigo_preparacion)
.then(result => {
console.log(result);
setIngsPreparaciones([...ingsPreparaciones, result.data]); //I want to update this value, however it appears as empty.
console.log(ingsPreparaciones);
});
});
}
return(
<div>
</div>
);}
Thank you for your help
You can use the hook useEffect the times you want. The only thing you must be aware is about the dependency array and be careful about combinations. You can even do this.
useEffect(()=> {
doSomethingWithFoo();
}, [foo]);
useEffect(()=> {
doSomethingWithBar();
}, [bar]);
useEffect(()=> {
doSomethingWithFooOrBar();
}, [foo, bar]);
Separate effects as per need. Also, make use of async-await pattern.
Like so:
const { objectS } = props;
useEffect(
async () => {
const result = await axios.get(`heroku_url/?codigo=${objectS}`);
setStockPreparaciones(result.data.preparaciones);
}
, [objectS]
);
// watcher for state updates
useEffect(
() => {
console.log(stockPreparaciones)
}
, [stockPreparaciones]
)

Using hooks for a simple fetch request and breaking the rules of hooks, unsure how?

I'm trying to create a simple fetch with hooks from an AWS database. At the moment it errors out and the only reason I can see is because it breaks the rules of hooks but I'm not sure how. It's at the top level of this functional component and it's not called inside an event handler.
The result of this call (an array of user data), needs to be exported as a function and called in another file.
If anyone can spot something I have missed and can highlighted how I'm breaking the rules of hooks I'd be grateful!
Thanks!
const FetchUsers = () => {
const [hasError, setErrors] = useState(false);
const [Users, setUsers] = useState({});
async function fetchData() {
const res = await fetch(
"USERSDATABASE"
);
res
.json()
.then(res => setUsers(res))
.catch(err => setErrors(err));
}
useEffect(() => {
fetchData();
}, []);
return Users;
};
export { FetchUsers };
consumed here....
class UsersManager {
constructor() {
this.mapUserCountries = {};
}
init() {
const mappedUsers = FetchUsers();
mappedUsers.forEach(user => {
const c = user.country;
if (!this.mapUserCountries[c])
this.mapUserCountries[c] = { nbUsers: 0, users: [] };
this.mapUserCountries[c].nbUsers++;
this.mapUserCountries[c].users.push(user);
});
}
getUsersPerCountry(country) {
return this.mapUserCountries[country];
}
}
export default new UsersManager();
The problem is that you are calling the FetchUsers inside a Class component, and the FetchUsers is executing a React Hook. This is not allowed by React.
First - Hooks don't work inside class based components.
Second - All custom hooks should start with use, in your case useFetchUsers. By setting use as prefix, react will track your hook for deps and calling in correct order and so on.

React useEffect hook - infinite loop with redux action that uses ID

I'm using useEffect in combination with reduct actions. I'm aware that I have to extract the action function from the props and provide it as the second argument which generally works for bulk fetches. But if I use an ID from the props as well, it ends up in an infinity loop:
export function EmployeeEdit(props) {
const { fetchOneEmployee } = props;
const id = props.match.params.id;
const [submitErrors, setSubmitErrors] = useState([]);
useEffect(() => fetchOneEmployee(id), [fetchOneEmployee, id]);
const onSubmit = employee => {
employee = prepareValuesForSubmission(employee);
props.updateEmployee(employee._id, employee)
.then( () => props.history.goBack() )
.catch( err => setSubmitErrors(extractErrors(err.response)) );
};
return (
<div>
<h3>Edit Employee</h3>
<NewEmployee employee={props.employee} employees={props.employees} onSubmit={onSubmit} submitErrors={submitErrors} />
</div>
)
}
EmployeeEdit.propTypes = {
fetchOneEmployee: PropTypes.func.isRequired,
employees: PropTypes.array.isRequired,
employee: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
employees: state.employees.items,
employee: state.employees.item
})
export default connect(mapStateToProps, { fetchOneEmployee, updateEmployee })(EmployeeEdit);
And the redux action:
export const fetchOneEmployee = (id) => dispatch => {
axios.get(`http://localhost:8888/api/users/${id}`)
.then(res => {
const employee = res.data;
dispatch({
type: FETCH_ONE_EMPLOYEE,
payload: employee
})
})
});
};
Anybody an idea what I'm doing wrong?
one of the values in your dependency array ([fetchOneEmployee, id]) is changing. It is hard to say which value it is with the limited code you have supplied.
At first glance though, you probably want fetchOne instead of fetchOneEmployee in your array.
Your inifite loop is probably caused because of fetchOneEmployee passed as argument to useEffect. Did you pass fetchOneEmployee to EmployeeEdit as arrow function? If you did then fetchOneEmployee always will be change.
This is a part of great article about react hooks fetch data.
https://www.robinwieruch.de/react-hooks-fetch-data
I especially recommended header CUSTOM DATA FETCHING HOOK

Categories

Resources