Firebase doc field shows an empty array after using useState() [duplicate] - javascript

This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Closed last month.
I'm new to react and I'm trying to make an app, where user can upload multiple images at once, and those images get sent to the firebase storage, and urls of those images get stored in an array inside a doc, so that later on they can be accessed in a different page.
Here is a snippet of my code:
function CreateCarParts({isAuth}) {
const [images, setImages] = useState(null);
const [imageUrls, setImageUrls] = useState([]);
let navigate = useNavigate();
const postsCollectionRef = collection(db, "CarParts")
useEffect(() => {
if (!localStorage.getItem('isAuth')){
navigate("/login");
};
}, []);
const uploadImages = async () => {
if(images === null) return;
let uniqueFolderName = new Date().getTime().toString();
const imageList = Array.from(images);
for(let i = 0; i < imageList.length; i++) {
const image = imageList[i];
const imageLinkName = `carParts/${uniqueFolderName}/${image.name}`;
const imageRef = ref(storage, imageLinkName);
const snapshot = await uploadBytes(imageRef, image);
const url = await getDownloadURL(snapshot.ref);
setImageUrls((prev) => [...prev, url]);
}
alert("Images Uploaded");
};
const createPost = async () => {
await uploadImages();
await addDoc(postsCollectionRef, {imageUrls});
navigate('/carparts');
};
return (
<>
<Form.Control onChange={(event) => {setImages(event.target.files);uploadImages();}} type="file" multiple />
<Button onClick={createPost}> Submit </Button>
</>
)
}
export default CreateCarParts;
The images get sent to the storage, and the doc is created successfully, however the imageUrls array is empty. I can't figure out why it's not working, I assume I'm using the useState() incorrectly.

imageUrls won't be updated in the same render cycle, so calling await addDoc(postsCollectionRef, {imageUrls}); right after await uploadImages(); won't work
I would store urls in a local array instead
function CreateCarParts({isAuth}) {
const [images, setImages] = useState(null);
let navigate = useNavigate();
const postsCollectionRef = collection(db, "CarParts")
useEffect(() => {
if (!localStorage.getItem('isAuth')){
navigate("/login");
};
}, []);
const uploadImages = async () => {
if(images === null) return;
let uniqueFolderName = new Date().getTime().toString();
const imageList = Array.from(images);
const urls = [];
for(let i = 0; i < imageList.length; i++) {
const image = imageList[i];
const imageLinkName = `carParts/${uniqueFolderName}/${image.name}`;
const imageRef = ref(storage, imageLinkName);
const snapshot = await uploadBytes(imageRef, image);
const url = await getDownloadURL(snapshot.ref);
urls.push(url);
}
alert("Images Uploaded");
return urls;
};
const createPost = async () => {
const imageUrls = await uploadImages();
await addDoc(postsCollectionRef, {imageUrls});
navigate('/carparts');
};
return (
<>
<Form.Control onChange={(event) => {setImages(event.target.files);uploadImages();}} type="file" multiple />
<Button onClick={createPost}> Submit </Button>
</>
)
}
export default CreateCarParts;

Related

How to retrieve content and its image from firebase using react js

I have managed to push content and image and the name of the image is also added to the database as a reference, and I have managed to retrieve only content. if anyone can help me out it will be great.
As you can see my Home.js code below
Home.js :
function Home(isAuth) {
const [postList, setPostList] = useState([]);
const [imageUrl, setImageUrl] = useState(undefined);
const postsCollectionRef = collection(db, "posts");
useEffect(() => {
const getPosts = async () => {
const data = await getDocs(postsCollectionRef);
setPostList(data.docs.map((doc) => ({ ...doc.data(), id: doc.id })));
};
const getImage = async () => {
const storage = getStorage()
.ref('/' + `${post.cover}`) //name in storage in firebase console
.getDownloadURL()
.then((url) => {
setImageUrl(url);
})
}
getPosts();
})
}

UseContext Doesn't Re-render when an component value is updated

I'm using React's context api to store an array of Favorite products.The favorites Array is filled with Boolean Value False and turned to true based on id of the products.There is collection page which displays productCards having an addtoFavorite button,Upon clicking the button disables but if the product is already present in favorites it has to disabled.
Now it works perfectly fine for the 1st Page , disabling only favorite products with the array containing values true and false based on index of the products but when navigated to another page it disables other products at the same index even though the favorites array is updated to have all values as false.If we Navigate Back or move to another page its value now remains false in the array.It looks as if UseContext updates the value of the array late or doesn't rerender on change.
I have tried implementing other stuffs but it still wouldn't re-render when the array was changed.
Here's the FavoritesContext:
const FavoritesContext = React.createContext({
addToFavorites: (id,index) => {},
favorites:[],
storedFavorites:(data) => {}
});
export const FavoritesContextProvider = (props) => {
const authCtx = useContext(AuthContext)
const token = authCtx.token;
const userId = authCtx.userId;
const [favorites,setFavorites] = useState([]);
// To retrieve stored favorites from FireBase
const retrieveStoredFavorites = (data) => {
let fav = new Array(data.length).fill(false);
setFavorites(fav);
let queryParams = '?auth=' + token + '&orderBy="userId"&equalTo="' + userId + '"';
axiosInstance.get('/Favorites.json' + queryParams)
.then((response) => {
let fetchProductData = [];
for (let key in response.data) {
fetchProductData.push({
...response.data[key],
productId: key,
});
}
let favoriteList = [];
//To find if the product is present in the Fetched Favorite products List
for(let i=0;i<data.length;i++){
let ids = data[i].id
let favoriteProducts = !!fetchProductData.find((product)=>product.id==ids)
favoriteList.push(favoriteProducts)
}
//console.log(favoriteList)
setFavorites(favoriteList)
});
}
//Add to Favorites
const addTofavoritesHandler = (Product,index) => {
axiosInstance
.post('Favorites.json?auth='+token,Product)
.then((response) => {
//console.log("SUCCESS")
})
.catch((error) => console.log(error));
let favoriteOnes = [...favorites];
favoriteOnes[index] = true;
setFavorites(favoriteOnes);
};
const contextValue = {
addToFavorites:addTofavoritesHandler,
favorites:favorites,
storedFavorites:retrieveStoredFavorites
};
return (
<FavoritesContext.Provider value={contextValue}>
{props.children}
</FavoritesContext.Provider>
);
};
export default FavoritesContext;
Now here is the Collection Page
const CollectionPage = () => {
const classes = useStyles();
const [products, setProducts] = useState([]);
const [filteredProducts, setFilteredProducts] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [productsPerPage] = useState(9);
const [loading, setLoading] = useState(false);
const { enqueueSnackbar } = useSnackbar();
const authCtx = useContext(AuthContext);
const token = authCtx.token;
const userId = authCtx.userId;
const favoriteCtx = useContext(FavoritesContext)
const favorites = favoriteCtx.favorites
//To Display the Products in Main Content
const DisplayProductsHandler = (Data) => {
//Get value of FirstPageNumber and LastPageNumber
const indexOfLastPage = currentPage * productsPerPage;
const indexOfFirstPage = indexOfLastPage - productsPerPage;
//console.log("[Products]")
const productData = Data.slice(indexOfFirstPage, indexOfLastPage);
favoriteCtx.storedFavorites(productData)
//console.log(productData);
const updatedProductData = productData.map((product,index) => {
return (
<ProductCard
Link={`/Info/${product.id}`}
key={product.id}
Title={product.productName}
Image={product.productImage}
Value={product.price}
addToFavorites={() => addTofavoritesHandler(product,index)}
addToCart={() => addToCartHandler(product)}
disableFavoriteButton={favorites[index]}
/>
);
});
setProducts(updatedProductData);
};
//Display the Products from DisplayProductHandler
useEffect(() => {
setLoading(true);
//Scroll To Top When Reloaded
window.scrollTo(0, 0);
//To Display the Products
if (filteredProducts.length === 0) {
DisplayProductsHandler(ProductData);
} else {
DisplayProductsHandler(filteredProducts);
}
setLoading(false);
}, [currentPage, filteredProducts]);
//Add to Favorites Handler
const addTofavoritesHandler =(likedProduct,index) => {
setLoading(true);
let updatedLikedProduct = {
...likedProduct,
userId: userId,
};
favoriteCtx.addToFavorites(updatedLikedProduct,index)
//To Display ADDED TO FAVORITES Message using useSnackbar()
enqueueSnackbar("ADDED TO FAVORITES", { variant: "success" })
setLoading(false);
};
I need it to re-render every time the array in context is updated.

i can console.log(data) in getCurrency function but can't asign data to setCurrency .it does'nt work

i can console.log value of data in getCurrency funuction.but when i try to put data into setCurrency and get it from currency it doesn't work.its just empty object then.
import React ,{useState, useEffect} from 'react';
import TranslateForm from './translateComponent';
const Main = () => {
const API_KEY = 'b8533a4d2a2297728b70';
const [rcurrency, setRcurrency] = useState('USD');
const [ccurrency, setCcurrency] = useState('LKR');
const [query, setQuery] = useState();
const [currency, setCurrency] = useState({});
useEffect(() => {
setCurrency(getCurrency());
console.log(currency);
},[]);
useEffect(
() => {
getQuery();
},[query]
);
const getCurrency = async() => {
const responce = await fetch(`https://free.currconv.com/api/v7/currencies?apiKey=${API_KEY}`);
const data = await responce.json();
console.log(data);
setCurrency(data);
}
const getQuery = async() => {
const responce = await fetch(`https://free.currconv.com/api/v7/convert?q=${rcurrency}_${ccurrency},${ccurrency}_${rcurrency}&compact=ultra&apiKey=${API_KEY}`);
const qdata = await responce.json();
setQuery(qdata[Object.keys(qdata)[0]]);
console.log(query);
}
return(
<div>
<TranslateForm/>
</div>
);
}
export default Main;
Actually the issue is your function is not returning any data in order to set that in the setCurrency method.
Either set data in your getCurrency() method or return the data from getCurrency and set in useEffect().
useEffect(() => {
const respCurrency = getCurrency();
respCurrency.then((data)=>{
setCurrency(respCurrency);
})
console.log(currency); // this will always give you default value
},[]);
const getCurrency = async() => {
const responce = await fetch(`https://free.currconv.com/api/v7/currencies?apiKey=${API_KEY}`);
const data = await responce.json();
console.log(data);
setCurrency(data);
return data;
}
return(
<div>
Data : JSON.stringify(currency) // Your currency logging
<TranslateForm/>
</div>
);
Also since getCurrency is an async funtion it will most probably be returning you a promise object.So use .then() method to assign to the setCurrency method.
Try consoling currency now in your render method you will get the data.
I hope this works.
Thanks

Fetching and displaying images from firebase on React.js app

so I am trying to fetch images from Firebase storage and then display them in a React component. Frankly, I'm new to React/Javascript and I'm having a hard time understanding the asynchronous nature of React/JS and I'm running into the problem where my images are fetched but the component is not re-rendered upon completion of the fetch...
This is the code on my main page, in the useEffect function, I am trying to fetch the images and then store their download urls in an array and then set that array in state (upon page load i.e. only once). Since these are promises, its not happening synchronously and when I first load the page it displays no images. If I, however, click on a different component on the page, it re-renders my Pictures component and the images show up(??), so I know the fetch has worked.
let storageRef = firebase.storage().ref()
let calendarRef = React.createRef()
const position = props.location.state.name.indexOf("#")
const username = props.location.state.name.substring(0, position);
const [simpleDate, setSimpleDate] = useState(null)
const [selectedDate, setSelectedDate] = useState('')
const [showModal, setShowModal] = useState(false)
const [showCancelModal, setShowCancelModal] = useState(false)
const [expectedPeople, setExpectedPeople] = useState(null)
const [events, setEvents] = useState([])
const [helpModal, showHelpModal] = useState(false)
const [pictureURLs, setPictureURLs] = useState([])
useEffect(() => {
//load pictures
const fetchImages = async () => {
let urls = []
storageRef.child('cabinPictures').listAll().then((result) => {
result.items.forEach((imageRef) => {
imageRef.getDownloadURL().then(url => {
urls.push(url)
})
})
})
return urls;
}
fetchImages().then(urls => {
setPictureURLs(urls);
console.log("inside .then() " + pictureURLs)
})
//fetch reservations
firebase
.firestore()
.collection('reservations')
.onSnapshot(serverUpdate => {
const reservations = serverUpdate.docs.map(_doc => {
const data = _doc.data();
data['id'] = _doc.id;
return data;
});
let fetchedEvents = reservations.map(reservation => {
let date = reservation.reservationDate.toDate()
const month = ("0" + (date.getUTCMonth() + 1))
let dateString = date.getUTCFullYear() + "-" + ("0" + (date.getUTCMonth()+1)).slice(-2) + "-" + ("0" + date.getUTCDate()).slice(-2)
return {title: reservation.username + " - " + reservation.numPeople + " total", date: dateString, id: reservation.id, totalPeople: reservation.numPeople, month: month}
})
console.log(fetchedEvents)
setEvents(fetchedEvents)
});
}, [])
My Pictures component in the main page where the useEffect (above) is run. I pass the urls from state as a prop:
<div className="pictures-div-container">
<Pictures pictureURLs={pictureURLs}>
</Pictures>
</div>
The code for my Picture component:
import React, { useState, useEffect } from 'react'
import styles from "./styles.css"
const firebase = require('firebase');
const Pictures = (props) => {
const [uploadImage, setUploadImage] = useState(null)
const [progressValue, setProgressValue] = useState(0)
let storageRef = firebase.storage().ref()
let { pictureURLs } = props
const handleUpload = () => {
setProgressValue(0)
const uploadTask = storageRef.child(`cabinPictures/${uploadImage.name}`).put(uploadImage)
uploadTask.on('state_changed',
(snapshot) => {
//progress function
const progress = Math.round((snapshot.bytesTransferred / snapshot.totalBytes ) * 100)
setProgressValue(progress)
},
(error) => {
//error function
console.log(error)
},
() => {
//complete function
storageRef.child('cabinPictures').child(uploadImage.name).getDownloadURL().then(url => {
console.log(url)
} )
});
}
const handleFileSelect = (e) => {
if (e.target.files[0]) {
setUploadImage(e.target.files[0])
}
}
return (
<div className="pictures-container">
<h2>Upload a Picture!</h2>
<button className="upload-button" onClick={() => handleUpload()}>Upload</button>
<input type="file" onChange={(e) => handleFileSelect(e)}></input>
<progress value={progressValue} max="100"></progress>
<div className="pictures">
{
pictureURLs.map((url, index) => {
return <img className="picture" key={index} src={url}></img>
})
}
</div>
</div>
)
}
export default Pictures
So, can anyone help me understand why the Pictures component is not re-rendering automatically when the state is set after fetching the picture urls from firebase? I thought that when a prop changes in a component, the whole component is re-rendered?
EDIT:
So this is what I changed in my main page's useEffect function as per the answer's suggestions, and it works!
//fetch and load pictures
const fetchImages = async () => {
let result = await storageRef.child('cabinPictures').listAll();
let urlPromises = result.items.map(imageRef => imageRef.getDownloadURL())
return Promise.all(urlPromises)
}
const loadImages = async () => {
const urls = await fetchImages()
setPictureURLs(urls)
}
loadImages()
You have to let all the nested promises resolve before you return urls
I am not very current on firebase API so am not sure if result.items is an actual array or an object that has a foreach method. Following should work if it is a js array
Try something like:
//load pictures
const fetchImages = async() => {
let result = await storageRef.child('cabinPictures').listAll();
/// map() array of the imageRef.getDownloadURL() promises
let urlPromises = result.items.map(imageRef => imageRef.getDownloadURL());
// return all resolved promises
return Promise.all(urlPromises);
}
const urls = await fetchImages()
setPictureURLs(urls);
console.log("inside .then() ", urls)

Load more implementation in ReactJs

I am trying to implement load more button for my small project GiF generator. First I thought of appending next set of 20 response at the bottom, but failed to do.
Next, I thought of implementing loading the next set of 20 results by simply removing the current one. I tried to trigger a method on click of button, but I failed to do so. Its updating the state on second click of load more and then never updating it again.
Please help me find what I am missing, I have started learning React yesterday itself.
import React, { useEffect, useState } from 'react';
import './App.css';
import Gif from './Gif/Gif';
const App = () => {
const API_KEY = 'LIVDSRZULELA';
const [gifs, setGif] = useState([]);
const [search, setSearch] = useState('');
const [query, setQuery] = useState('random');
const [limit, setLimit] = useState(20);
const [pos, setPos] = useState(1);
useEffect(() => {
getGif();
}, [query])
const getGif = async () => {
const response = await fetch(`https://api.tenor.com/v1/search?q=${query}&key=${API_KEY}&limit=${limit}&pos=${pos}`);
const data = await response.json();
setGif(data.results);
console.log(data.results)
}
const updateSearch = e => {
setSearch(e.target.value);
}
const getSearch = e => {
e.preventDefault();
setQuery(search);
setSearch('');
}
const reload = () => {
setQuery('random')
}
const loadMore = () => { // this is where I want my Pos to update with 21 on first click 41 on second and so on
let temp = limit + 1 + pos;
setPos(temp);
setQuery(query);
}
return (
<div className="App">
<header className="header">
<h1 className="title" onClick={reload}>React GiF Finder</h1>
<form onSubmit={getSearch} className="search-from">
<input className="search-bar" type="text" value={search}
onChange={updateSearch} placeholder="type here..." />
<button className="search-button" type="submit">Search</button>
</form>
<p>showing results for <span>{query}</span></p>
</header>
<div className="gif">
{gifs.map(gif => (
<Gif
img={gif.media[0].tinygif.url}
key={gif.id}
/>
))}
</div>
<button className="load-button" onClick={loadMore}>Load more</button>
</div>
);
}
export default App;
Please, help me find, what I am doing wrong, As I know the moment I will update setQuery useEffect should be called with new input but its not happening.
Maybe try something like this:
// Fetch gifs initially and then any time
// the search changes.
useEffect(() => {
getGif().then(all => setGifs(all);
}, [query])
// If called without a position index, always load the
// initial list of items.
const getGif = async (position = 1) => {
const response = await fetch(`https://api.tenor.com/v1/search?q=${query}&key=${API_KEY}&limit=${limit}&pos=${position}`);
const data = await response.json();
return data.results;
}
// Append new gifs to existing list
const loadMore = () => {
let position = limit + 1 + pos;
setPos(position);
getGif(position).then(more => setGifs([...gifs, ...more]);
}
const getSearch = e => {
e.preventDefault();
setQuery(search);
setSearch('');
}
const updateSearch = e => setSearch(e.target.value);
const reload = () => setQuery('random');
Basically, have the getGifs method be a bit more generic and then if loadMore is called, get the next list of gifs from getGift and append to existing list of gifs.

Categories