I'm trying to get some images from firebase storage loaded into a react-responsive-carousel. The problem is that only 1 or 2 or even none of the images are loaded (there are at least 10 images). This is what I'm trying:
const [imagenes, setImagenes] = useState([]);
useEffect(() => {
const fetchedImagenes = [];
firebase.storage().ref().child('PR/Gallery').listAll().then(function (result) {
result.items.forEach(function (itemRef) {
itemRef.getDownloadURL().then(function (link) {
const fetchedImagen = link
fetchedImagenes.push(fetchedImagen)
})
});
setImagenes(fetchedImagenes)
})
}, [])
And this is the carousel code:
<Carousel dynamicHeight={true}>
{imagenes.map(imagen => {
return <div>
<img src={imagen} />
</div>
})}
</Carousel>
My guess is that the imagenes array is not filled when I show the images, so is there any way that I can wait for the imagenes array to be filled and then show the images?
You're now calling setImagenes(fetchedImagenes) before any of the fetchedImagenes.push(fetchedImagen) has been called, so you're setting an empty array.
The quick fix is to set the array to the state whenever a download URL is determined by moving it into the then callback:
firebase.storage().ref().child('PR/Gallery').listAll().then(function (result) {
result.items.forEach((itemRef) => {
itemRef.getDownloadURL().then((link) => {
const fetchedImagen = link
fetchedImagenes.push(fetchedImagen)
setImagenes(fetchedImagenes)
})
});
})
This will work, but may result in some flicker as you're updating the UI whenever any download URL is determined.
If you want to only update the state/UI once, you're looking for Promise.all and it'll be something like:
firebase.storage().ref().child('PR/Gallery').listAll().then(function (result) {
const promises = result.items.map((itemRef) => itemRef.getDownloadURL());
Promise.all(promises).then((urls) =>
setImagenes(urls)
});
})
Related
I'm new to React and I'm trying to render a list of Pokemons.
I'm fetching the pokemon names from a local file and then using those names to trigger HTTP calls to my backend server, to get the pokemon images. Here's my code so far:
function PokemonList(props) {
const [pokemonList, setPokemonList] = useState([]);
const [isFetched, setIsFetched] = useState(false);
const [renderedList, setRenderedList] = useState([]);
useEffect(() => {
fetch(raw)
.then((r) => r.text())
.then((text) => {
setPokemonList(text.split("\n"));
setIsFetched(true);
});
}, []);
// I believe this is to blame, but I don't know why. On refresh, pokemonList becomes empty
useEffect(() => {
setRenderedList(populateRenderedList(pokemonList));
}, []);
return !isFetched ? null : (
<div className="list">
{renderedList}
<PaginationBar listSize={renderedList.length} list={renderedList} />
</div>
);
}
function populateRenderedList(pokemonList) {
let pokemonAPI = "https://pokeapi.co/api/v2/pokemon-form/";
const temp = [];
console.log(pokemonList);
pokemonList.forEach((pokemonName) => {
let renderedPokemon = (
<a className="pokemonLink" href={pokemonAPI + pokemonName.toLowerCase()}>
<PokemonDiv name={pokemonName.toLowerCase()} />
<h3>{pokemonName}</h3>
</a>
);
temp.push(renderedPokemon);
});
return temp;
}
As I have commented on the code, the 'pokemonList' renders fine when I make any changes to the PokemonList function. But the moment I refresh my page, 'pokemonList' becomes empty. Why is that?
I previously was not using 'useState' to populate my 'renderedList' list. So I believe the problem is happening because I'm using 'useState' , but I don't know why that's happening.
I had tried making 'renderedList' not a state, but I had to, for I am thinking about passing it as props to another child component, in order to change it's state.
So I have a component where I have to make an API call to get some data that has IDs that I use for another async API call. My issue is I can't get the async API call to work correctly with updating the state via spread (...) so that the checks in the render can be made for displaying specific stages related to specific content.
FYI: Project is a Headless Drupal/React.
import WidgetButtonMenu from '../WidgetButtonMenu.jsx';
import { WidgetButtonType } from '../../Types/WidgetButtons.tsx';
import { getAllInitaitives, getInitiativeTaxonomyTerm } from '../../API/Initiatives.jsx';
import { useEffect } from 'react';
import { useState } from 'react';
import { stripHTML } from '../../Utilities/CommonCalls.jsx';
import '../../../CSS/Widgets/WidgetInitiativeOverview.css';
import iconAdd from '../../../Icons/Interaction/icon-add.svg';
function WidgetInitiativeOverview(props) {
const [initiatives, setInitiatives] = useState([]);
const [initiativesStages, setInitiativesStage] = useState([]);
// Get all initiatives and data
useEffect(() => {
const stages = [];
const asyncFn = async (initData) => {
await Promise.all(initData.map((initiative, index) => {
getInitiativeTaxonomyTerm(initiative.field_initiative_stage[0].target_id).then((data) => {
stages.push({
initiativeID: initiative.nid[0].value,
stageName: data.name[0].value
});
});
}));
return stages;
}
// Call data
getAllInitaitives().then((data) => {
setInitiatives(data);
asyncFn(data).then((returnStages) => {
setInitiativesStage(returnStages);
})
});
}, []);
useEffect(() => {
console.log('State of stages: ', initiativesStages);
}, [initiativesStages]);
return (
<>
<div className='widget-initiative-overview-container'>
<WidgetButtonMenu type={ WidgetButtonType.DotsMenu } />
{ initiatives.map((initiative, index) => {
return (
<div className='initiative-container' key={ index }>
<div className='top-bar'>
<div className='initiative-stage'>
{ initiativesStages.map((stage, stageIndex) => {
if (stage.initiativeID === initiative.nid[0].value) {
return stage.stageName;
}
}) }
</div>
<button className='btn-add-contributors'><img src={ iconAdd } alt='Add icon.' /></button>
</div>
<div className='initiative-title'>{ initiative.title[0].value } - NID ({ initiative.nid[0].value })</div>
<div className='initiative-description'>{ stripHTML(initiative.field_initiative_description[0].processed) }</div>
</div>
);
}) }
</div>
</>
);
}
export default WidgetInitiativeOverview;
Here's a link for video visualization: https://vimeo.com/743753924. In the video you can see that on page refresh, there is not data within the state but if I modify the code (like putting in a space) and saving it, data populates for half a second and updates correctly within the component.
I've tried using spread to make sure that the state isn't mutated but I'm still learning the ins and outs of React.
The initiatives state works fine but then again that's just 1 API call and then setting the data. The initiativeStages state can use X amount of API calls depending on the amount of initiatives are returned during the first API call.
I don't think the API calls are necessary for this question but I can give reference to them if needed. Again, I think it's just the issue with updating the state.
the function you pass to initData.map() does not return anything, so your await Promise.all() is waiting for an array of Promise.resolve(undefined) to resolve, which happens basically instantly, certainly long before your requests have finished and you had a chance to call stages.push({ ... });
That's why you setInitiativesStage([]) an empty array.
And what you do with const stages = []; and the stages.push() inside of the .then() is an antipattern, because it produces broken code like yours.
that's how I'd write that effect:
useEffect(() => {
// makes the request for a single initiative and transforms the result.
const getInitiative = initiative => getInitiativeTaxonomyTerm(
initiative.field_initiative_stage[0].target_id
).then(data => ({
initiativeID: initiative.nid[0].value,
stageName: data.name[0].value
}))
// Call data
getAllInitaitives()
.then((initiatives) => {
setInitiatives(initiatives);
Promise.all(initiatives.map(getInitiative))
.then(setInitiativesStage);
});
}, []);
this code still has a flaw (imo.) it first updates setInitiatives, then starts to make the API calls for the initiaives themselves, before also updating setInitiativesStage. So there is a (short) period of time when these two states are out of sync. You might want to delay setInitiatives(initiatives); until the other API requests have finished.
getAllInitaitives()
.then(async (initiatives) => {
const initiativesStages = await Promise.all(initiatives.map(getInitiative));
setInitiatives(initiatives);
setInitiativesStage(initiativesStages)
});
I have built a chat using Firebase and ReactJS. I mainly followed their Firebase's web codelab at https://firebase.google.com/codelabs/firebase-web#1. However, I have gotten stuck on the image uploading functionality. Since I am using ReactJS, I have had to modify their plain JS code to match mine. I am able to save a message with a "loading" image url in Firestore, then, I successfully save the image that I want to ultimately show in the chat in Firebase Storage, and finally then I successfully retrieve its url from Storage and replace it with the url of the loading image in Firestore. The image does show in the chat, however, the loading image is not actually replaced but, instead, it remains in the chat when I want it to be completely replaced, obviously, so that the loading image is no longer there. Here's what I mean, in this image:
As you can see the loading image on top stayed on instead of being replaced by the image underneath it. I think it should be filtered out somehow before I save the new snapshot with the new image url. However, I can not figure out how to do it correctly. I tried to filter it out based on the url of the loading image which is saved locally but since it is saved as a base64 in Storage, it did not work. Neither did using the actual Base64 code as a way to filter it out. So, I need help to solve this issue. The codelab does not really specify this nor is it clear how they do it in their code which is in plain Javascript anyways and I use ReactJS so it may not be 100% suitable.
Here's, I believe, enough code to see what is going on. Let me know if you need more of it.
Here's how I send images to the Chat: (modeled on the Firebase codelab)
sendImageToChat () {
this.state.chatFiles.forEach((file) => {
firebase.firestore().collection('Chats')
.doc(this.state.uid)
.collection('Messages')
.add({
docId: this.state.docId,
imageUrl: loadingLogo,
timestamp: new Date(),
uid: this.state.uid,
name: this.state.displayName,
email: this.state.email
})
.catch((error) => {
this.setState({ writeError: error.message });
})
.then((messageRef) => {
// 2 - Upload the image to Cloud Storage.
const filePath = `users/${this.state.displayName}/${this.state.uid}/${moment().format("MMM Do YY")}/${uuidv4()}/${file.name}`
return firebase.storage().ref(filePath).put(file).then((fileSnapshot) => {
// 3 - Generate a public URL for the file.
return fileSnapshot.ref.getDownloadURL().then((url) => {
// 4 - Update the chat message placeholder with the image's URL.
return messageRef.update({
imageUrl: url,
storageUri: fileSnapshot.metadata.fullPath
});
});
});
}).catch(function(error) {
console.error('There was an error uploading a file to Cloud Storage:', error);
});
})
this.setState({
chatFiles: []
})
document.getElementById('file-1').value = "";
}
Here's how I, then, setState when the loading image is added and then when its url is modified: (Notice how I try to filter out the loadingLogo which is the loading image out of the state but it does not obviously work for the reason explained above).
startChat () {
document.getElementById("myForm").style.display = "block";
const ref = firebase.firestore().collection('Chats').doc(this.state.uid).collection('Messages');
const query = ref.orderBy('timestamp', 'desc').limit(10)
this.unsubFromMessages = query.onSnapshot((snapshot) => {
if (snapshot.empty) {
console.log('No matching documents.');
firebase.firestore().collection('Chats').doc(this.state.uid).
set({
name: this.state.displayName,
uid: this.state.uid,
email: this.state.email
}).then(console.log("info saved"))
.catch((error) => {
console.log("Error saving info to document: ", error);
});
}
snapshot.docChanges().reverse().forEach((change) => {
if (change.type === 'removed') {
console.log(change.doc.data().content)
} else if (change.type === 'added') {
this.setState(state => {
const messages = [...state.messages, {id: change.doc.id, body: change.doc.data()}]
return {
messages
}
})
setTimeout( this.scrollToBottom(), 2000)
} else if (change.type === 'modified') {
const filteredMessages = this.state.messages.filter(message => message.imageUrl !== loadingLogo)
console.log(filteredMessages)
this.setState(state => {
const messages = [...filteredMessages, {id: change.doc.id, body: change.doc.data()}]
return {
messages
}
})
setTimeout( this.scrollToBottom(), 2000)
}
});
}, (error) => {console.log(error)});
}
This is part of the Chat's JSX:
<div className="chatArea" id='messages'>
{
this.state.messages.map((message, index) => {
return message.body.uid === this.state.uid
?
<div>
{
message.body.imageUrl ?
<img src={message.body.imageUrl} className="message-sent"></img>
:
<p className="message-sent" key={index}>{message.body.content}</p>
}
</div>
:
<p className="message-received" key={index}>{message.body.content}</p>
})
}
<div style={{ float:"left", clear: "both" }}
ref={(el) => { this.myRef = el; }}>
</div>
</div>
I know the issue is not with Firebase but rather with ReactJS. I know I need to remove, filter out, replace or delete that loading image before or after the modified message with the new url is saved to the state. So, please help me figure this out. I am sure many people may encounter this problem.
Thank you!
I figured it out. I might as well delete this question but it may help someone build a chat with ReactJS and Firebase. Anyways, my approach to filter out based on the object property, imageUrl is a viable option. It works! My silly oversight was that I did not add the parent property or object, "body", after the object "message". More specifically, instead of const filteredMessages = this.state.messages.filter(message => message.imageUrl !== loadingLogo), it should be const filteredMessages = this.state.messages.filter(message => message.body.imageUrl !== loadingLogo). You can also try to add an object property that you can use to filter out messages with, for example, allowed: yes or no. If you need more clarification, just ask me, I am glad to help. Happy coding!
I'm writing a React App, where I have a Fallback component, which gets displayed when something goes wrong, for example: network is down, API isn't reachable, unknown route, etc.
This component will fetch some URLs of cat pictures and displays a slide show.
But of course this isn't possible when the network is down.
Though I'd like to somehow create and initialize this component in the background when the App starts, so everything is ready in the case of emergency.
Additional info: The Fallback component will be used as child component of different views. So it's not possible to simply mount it in App.jsx and use CSS visibility: hidden / visible to hide and display it.
Is this possible and does someone know how to do it?
EDIT: Example code
const Fallback = () => {
// will contain urls like:
// - https://cats.example.org/images/foo.jpg
// - https://cats.example.org/images/bar.png
// - https://cats.example.org/images/42.gif
const [urls, setUrls] = useState([]);
useEffect(() => {
fetch('https://catpictures.example.org')
.then(response => response.json())
.then(data => setUrls(data));
}, []);
// this should be cached somehow:
return (
<div>
{urls.map(url =>
<img src={url} />
}
</div>
);
}
You can do this and I've done it in big production apps by simply creating a new Image() and setting the src. The image will be preloaded when the component is first rendered.
const LoadingComponent() {
useEffect(() => {
const img = new Image();
img.src = imgUrl;
}, []);
return null; // doesn't matter if you display the image or not, the image has been preloaded
}
It could even become a hook such as useImagePreloader(src) but that's up to you.
Here is a Sandbox with a working version.
Steps to try it:
Create an incognito window, open devtools and check the network tab, search for "imgur". The image is loaded.
Set the network offline or disconnect from your WIFI.
Click on the Show Image button. The image will display correctly.
This solution will always work provided your cache settings for images are set correctly (usually they are). If not, you can save the images to blobs and get a URL to that blob, that will work 100% of times.
As you noted that you need an array of images, you can do that same code inside a loop and it will work just fine, images will still be cached:
const images = ['first.jpg', 'second.png', 'etc.gif'];
images.forEach(imageUrl => {
const img = new Image();
img.src = image
});
How about a service worker that would cache your assets and then serve them when offline? Then you could send a message with the URL to change to notify your "app" it is back online with some new content.
Thre is a working example here: https://serviceworke.rs/strategy-cache-update-and-refresh_demo.html
var CACHE = 'cache-update-and-refresh';
self.addEventListener('install', function(evt) {
console.log('The service worker is being installed.');
evt.waitUntil(caches.open(CACHE).then(function (cache) {
cache.addAll([
'./controlled.html',
'./asset'
]);
}));
});
function fromCache(request) {
return caches.open(CACHE).then(function (cache) {
return cache.match(request);
});
}
function update(request) {
return caches.open(CACHE).then(function (cache) {
return fetch(request).then(function (response) {
return cache.put(request, response.clone()).then(function () {
return response;
});
});
});
}
function refresh(response) {
return self.clients.matchAll().then(function (clients) {
clients.forEach(function (client) {
var message = {
type: 'refresh',
url: response.url,
eTag: response.headers.get('ETag')
};
client.postMessage(JSON.stringify(message));
});
});
}
self.addEventListener('fetch', function(evt) {
console.log('The service worker is serving the asset.');
evt.respondWith(fromCache(evt.request));
evt.waitUntil(
update(evt.request)
.then(refresh)
);
});
Local Storage
On page/App load;
Get each image
Save base64 data to localStorage
On network fail
Render <FallBack />
<FallBack />
Read localStorage
Render base64 images
Small example
We use fetch to get the cat images in the <App/> component
Save those to the localStorage
(NOTE: StackSnippet doesn't allow localStorage, so please test it on JSFiddle)
We use a useState to 'fake' the network status
// Init
const { useState } = React;
// <Fallback />
const Fallback = () => {
// Get all localstorage items
let ls = { ...localStorage };
// Get all starting with 'image_'
ls = Object.keys(ls).filter(key => key.startsWith('image_'));
// Render images
return (
<div>
<p>{'FallBack'}</p>
{
(ls.length < 1)
? 'Unable to find cached images!'
: (
ls.map((key) => {
// Get current key from localstorage
const base64 = localStorage.getItem(key);
// Render image
return <img src={base64} />;
})
)
}
</div>
);
}
// <App />
const App = ({title}) => {
const [network, setNetwork] = useState(true);
const [urls, setUrls] = useState([ 'https://placekitten.com/200/300', 'https://placekitten.com/200/300']);
// Render Fallback on lost network
if (!network) {
return <Fallback />;
}
// While network is active, get the images
urls.forEach((url, index) => {
fetch(url)
.then(response => response.blob())
.then(blob => {
// Convert BLOB to base64
var reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function() {
// Write base64 to localstorage
var base64data = reader.result;
localStorage.setItem('image_' + index, base64data);
console.log('Saving image ' + index + ' to localstorage');
};
});
})
return (
<div>
<p>{'App'}</p>
<p>Press me to turn of the internet</p>
<button onClick={() => setNetwork(false)}>{'Click me'}</button>
</div>
);
};
// Render <App />
ReactDOM.render(<App />, document.getElementById("root"));
JSFiddle Demo
Pros;
LocalStorage will not be cleared, if the same app is loaded the next day, we don't need to get those images again
Cons;
There's a size limit for LocalStorage
You can manually add a resource to the cache by using a preloaded <link>:
<link rel="preload" as="image" href="https://cats.example.org/images/foo.jpg">
Put this inside your index.html and use it from cache when needed by using the same href.
I have a problem when I'm trying to load a picture from my Flamelink schema that is connected to my Firebase DB.
I have a simple component that displays a puppy for sale:
const PuppyCard = (props) => {
return (
<div className='PuppyCard'>
<div className='PuppyCard__img'>
<img src={props.data.picture} alt={props.data.name} />
</div>
<div className='PuppyCard__text'>
<h2>{props.data.name}</h2>
<h3>{props.data.sex}</h3>
<h4>{props.data.age}</h4>
<h4>{props.data.price}</h4>
</div>
</div>
)
}
And as you can see there is a props.data.picture as my src for the image, but when i try to load it on the website i get everything but the picture. After looking into my DB i saw that the picture is not a file but a reference to the other folder created by flamelink:
1
This is what i am fetching from the DB right now.
getPuppies = () => {
db
.collection('fl_content')
.get()
.then(docs => {
if (!docs.empty) {
let allPuppies = []
docs.forEach(function (doc) {
const puppy = {
id: doc,
...doc.data()
}
allPuppies.push(puppy)
})
this.setState({
puppies: allPuppies
}, () => {
this.setState({
isLoaded: true
})
})
}
})
}
Any help is appreciated!
I've managed to solve this problem, although now i realize that the question is not accurate.
What I was trying to do was to get the picture that was uploaded in the Flamelink CMS to display on the website, however i was only getting a file ID when i tapped into the property of the props.
My solution to this problem was:
store the fileID in a variable
Query the folder created by the flamelink CMS inside the Cloud Firestore called "fl_files" =>
db.collection('fl_files').doc(fileID).get().then(function (doc) {
var fileName = doc.Nf.nn.proto.mapValue.fields.file.stringValue;
console.log(fileName);
storageRef.child('flamelink/media/' + fileName).getDownloadURL().then(function (url) {
console.log(url);
var img = document.getElementById('an id for the image inside a react child component');
img.src = url
})
Please note that 'flamelink/media/' is inside the Storage, not inside Cloud Firestore.
Hope this helps somebody with the same problem I've had!
Issue closed :)