React JS - How to update image only once loaded? - reactjs

I have a React app that receives a stream of image URIs from an EventSource. I want to display the most recent image, and only replace the image once the next image is fully loaded and can be displayed instantaneously. How do I accomplish this?
Edit 2: My current implementation does exactly this, except for one critical component: the most recent image is displayed as it loads, changing the vertical arrangement of the page.
Edit: Adding relevant snippets of code, this is my EventSource:
useEffect(() => {
let serverImageURIsEventSource = new EventSource(
url,
{withCredentials: false})
serverImageURIsEventSource.addEventListener('open', () => {
console.log('SSE opened!');
});
serverImageURIsEventSource.addEventListener('message', (e) => {
console.log(e.data);
setImageURIs([e.data]); // e.data is a string to the image URI
});
serverImageURIsEventSource.addEventListener('error', (e) => {
console.error('Error: ', e);
});
serverImageURIsEventSource.addEventListener('close', () => {
console.log('SSE closed!');
serverImageURIsEventSource.close();
});
return () => {
serverImageURIsEventSource.close();
};
}, [prompt]);
And this is my JSX with the images.
<ImageList cols={1}>
{imageURIs.map((imageURI) => (
<ImageListItem key={imageURI}>
<img
src={`${imageURI}`}
srcSet={`${imageURI}`}
loading="lazy"
// loading="eager" default
/>
</ImageListItem>
))}
</ImageList>

Make imageURIs as an array of object instead of array and a assign a number prop as your reference to order it by when you render it.
setImageURIs(prevImageUris => [...prevImageUris, {uri: e.data, number: prevImageUris.length + 1}])
and use sort when rendering
{imageURIs.sort((a,b) => b.number - a.number).map((imageURI) => (
<ImageListItem key={imageURI.uri}>
<img
src={`${imageURI.uri}`}
srcSet={`${imageURI.uri}`}
loading="lazy"
// loading="eager" default
/>
</ImageListItem>
))}

Related

React Upload +5000 files - Failed to load resource: net::ERR_INSUFFICIENT_RESOURCES

I'm building an upload files functionality using dropzone and react, my issue is that this application should support thousands of images and once I got about 1500 images it collapse and stop sending requests, then in the browser I got this issue:
Failed to load resource: net::ERR_INSUFFICIENT_RESOURCES
I saw some workarounds to create batches, but honestly don't know how to create it, since I'm processing the uploads one by one using async functions, this is my code:
const Dropzone = ({stateChanger, folder, props, ...rest}) => {
let container;
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}
let { id } = useParams();
const [files, setFiles] = useState([]);
const {getRootProps, getInputProps} = useDropzone({
onDrop: async acceptedFiles => {
stateChanger(true)
setFiles(acceptedFiles.map(file => Object.assign(file, {
preview: URL.createObjectURL(file),
uid: uuidv4()
})));
const url = `${API_URL}/projects/upload`;
let requestArr = [];
await asyncForEach(acceptedFiles, async (file) => {
console.log('file',file)
var formData = new FormData();
formData.append('file', file);
formData.append('projectId', id);
formData.append('rootfolder', folder);
console.log('formData',formData)
requestArr.push(
axios
.post(url,formData)
.then((rst) => {
console.log('rst',rst)
var elem = document.getElementById(file.uid);
var html = elem.innerHTML;
elem.innerHTML = `<span class="upload-success" >Uploaded</span>`;
})
.catch((error) => {
console.log('error', error);
var elem = document.getElementById(file.uid);
var html = elem.innerHTML;
elem.innerHTML = `<span class="upload-error" >Error uploading</span>`;
})
);
});
Promise.all(requestArr).then(() => {
console.log('resolved promise.all')
stateChanger(false)
});
}
});
const thumbs = files.map(file => (
<div className="thumb" key={file.name}>
<div className="thumbList">
{file.path} - {file.size} bytes - <span id={file.uid}><span className="upload-pending" >Uploading</span></span>
</div>
</div>
));
useEffect(() => {
// Make sure to revoke the data uris to avoid memory leaks
files.forEach(file => URL.revokeObjectURL(file.preview));
}, [files]);
return (
<section className="container">
<ToastContainer
ref={ref => container = ref}
className="toast-top-right"
/>
<div {...getRootProps({className: 'dropzone'})}>
<input {...getInputProps()} />
<img src={uploadImage} />
<br />
<p>Drag and drop your files, or click the button to upload</p>
<button className="btn primary-btn-active width50">Upload from computer</button>
</div>
<aside >
{thumbs}
</aside>
</section>
);
}
export default Dropzone
And the implementation pretty standard:
<Dropzone stateChanger={setNextDisable} folder={folder} />
Any suggestion?
Why would you need 1500, 5000 images at once?
The user can focus on only so much on the screen at once - and then you have scroll bars which work but is not ideal.
The solution in many places is to have it paginated (10, 20, 50) items on screen at once.
More than that is asking for trouble - as a developer you might have a more powerful PC than an average user - so likely most of your users will have more bottleneck problems, performance issues.
The solution in my opinion would be to reduce the number of items on screen because it doesn't make sense to have that massive amount of assets, and more than a 20 items and user can't focus or bother to have a look or try to do stuff.
Resource consumption has always been a problem that needs to be researched. The processing of large amounts of data is also a similar problem. if only upload. package and compress before uploading.
The problem is not related to Dropzone library. It just means your server, whatever you use, cannot handle such amount of traffic/connections. If you really need you should probably dedicate more resources as the error message states.

Data is not rendered when changing pages with react router

I am using the Link component from React-Router to change pages, on the Play.tsx I have data that I am getting using useEffect from Firestore and that data is rendered on page using map function if the array length is more than 1. It works fine until I change the page and go back to /play where Play.tsx is rendered and there is no data at all, the data from firestore is ok but nothing is rendered on the page.
If I am using a normal a tag instead of Link will work fine.
Play.tsx
const [users, setUsers] = useState<any>([]);
// code...
useEffect(() => {
db.collection("users").orderBy("points", "desc").limit(10).get().then((data: any) => {
data.forEach((usr: any) => {
users.push(usr.data());
console.log(users)
})
}).catch((err) => {
console.log("An error has occured: ", err.message)
})
}, [])
return (
<>
{
users.length >= 1 ?
users.map((d: any) => {
return (
<div className="userWrapper" key={Math.random() * 9999999999999999999}>
<Link to={`/user/${d.username}`}><img className="lbImage" src={d.profileImage} /></Link>
<Link to={`/user/${d.username}`}>
<p className="lbUser">{d.username} {d.pro ? <img src={Pro} className="lbPro" /> : null}
</p>
</Link>
<p className="lbPoints">{d.points} Points, {d.rank} ({d.races} Tests)</p>
</div>
)
})
: <div className="lbSpinner"></div>
}
</>
)
OtherComponent.tsx
<Link to="/play"><li>Home</li></Link>
My route: Play.tsx (data rendered) -> OtherComponent.tsx -> Play.tsx (Data is not rendered)
Note: The problem is not at the data itself because I can console.log it, the problem is at the DOM, is like the map function on users is not calling.
Note2: I have a condition there, if I have more than 1 user on the array it will map, if not it will show a spinner, but none of these is rendered when I come back to Play.tsx, and no error is consoled.
In your useEffect hook you are mutating the state directly by doing users.push(usr.data())
Should be using the setUsers function from the useState hook.
useEffect(() => {
db.collection("users").orderBy("points", "desc").limit(10).get().then((data: any) => {
data.forEach((usr: any) => {
setUsers((prevState)=> ([...prevState, usr.data()]));
console.log(users)
})
}).catch((err) => {
console.log("An error has occured: ", err.message)
})
}, [])
Try changing that and see if the state is working properly

How to load images from firebase to carousel react?

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)
});
})

Loading data on screen load

I have following code, Right now renderProductItem is rendered with fixed products. I want list to be rendered on screen load. So when screen is loaded it should call an API and render the list based on API response.
I saw solutions using state sopmething like https://github.com/vikrantnegi/react-native-searchable-flatlist/blob/master/src/SearchableList.js but the problem is when i create constructer its not getting called on screen load. So i am not sure how to use state in my case.
I am unable to figure out how to call an API on screen load and render list once response is available.
export const ProductListScreen = ({ navigation, route }): React.ReactElement => {
const displayProducts: Product[] = products.filter(product => product.category === route.name);
const renderProductItem = (info: ListRenderItemInfo<Product>): React.ReactElement => (
<Card
style={styles.productItem}
header={() => renderItemHeader(info)}
footer={() => renderItemFooter(info)}
onPress={() => onItemPress(info.index)}>
<Text category='s1'>
{info.item.title}
</Text>
<Text
appearance='hint'
category='c1'>
{info.item.category}
</Text>
<RateBar
style={styles.rateBar}
value={4}
// onValueChange={setRating}
/>
<CategoryList
style={styles.categoryList}
data={["Adventure", "Sport", "Science", "XXX"]}
/>
<Text>
The element above represents a flex container (the blue area) with three flex items.
</Text>
</Card>
);
return (
<List
contentContainerStyle={styles.productList}
data={displayProducts.length && displayProducts || products}
renderItem={renderProductItem}
/>
);
};
You can use hooks in your ProductListScreen component. You can create a state using useState hook and with the help of useEffect you achieve the behaviour of componentDidMount.
Please refer to this link:
https://reactjs.org/docs/hooks-effect.html
Use componentDidMount() to display the content. In React, this is a "lifecycle method". In the examples cited in the documentation you see that functions can be triggered when a component is added or removed. Based on your description you would want to test out componentDidMount for your code.
In the code sample you cited, you can see that this developer uses it in his class to call the makeRemoteRequest function
componentDidMount() {
this.makeRemoteRequest();
}
makeRemoteRequest = () => {
const url = `https://randomuser.me/api/?&results=20`;
this.setState({ loading: true });
fetch(url)
.then(res => res.json())
.then(res => {
this.setState({
data: res.results,
error: res.error || null,
loading: false,
});
this.arrayholder = res.results;
})
.catch(error => {
this.setState({ error, loading: false });
});
};

React function not working from child component

I am trying to get a function working which removes an image uploaded using React Dropzone and react-sortable.
I have the dropzone working, and the sort working, but for some reason the function I have on the sortable item which removes that particular item from the array does not work.
The onClick event does not seem to call the function.
My code is below.
const SortableItem = SortableElement(({value, sortIndex, onRemove}) =>
<li>{value.name} <a onClick={() => onRemove(sortIndex)}>Remove {value.name}</a></li>
);
const SortableList = SortableContainer(({items, onRemove}) => {
return (
<ul>
{items.map((image, index) => (
<SortableItem key={`item-${index}`} index={index} value={image} sortIndex={index} onRemove={onRemove} />
))}
</ul>
);
});
class renderDropzoneInput extends React.Component {
constructor (props) {
super(props)
this.state = { files: [] }
this.handleDrop = this.handleDrop.bind(this)
}
handleDrop (files) {
this.setState({
files
});
this.props.input.onChange(files)
}
remove (index){
var array = this.state.files
array.splice(index, 1)
this.setState({files: array })
this.props.input.onChange(array)
}
onSortEnd = ({oldIndex, newIndex}) => {
this.setState({
files: arrayMove(this.state.files, oldIndex, newIndex),
});
};
render () {
const {
input, placeholder,
meta: {touched, error}
} = this.props
return (
<div>
<Dropzone
{...input}
name={input.name}
onDrop={this.handleDrop}
>
<div>Drop your images here or click to open file picker</div>
</Dropzone>
{touched && error && <span>{error}</span>}
<SortableList items={this.state.files} onSortEnd={this.onSortEnd} onRemove={(index) => this.remove(index)} />
</div>
);
}
}
export default renderDropzoneInput
Update: This was caused by react-sortable-hoc swallowing click events. Setting a pressDelay prop on the element allowed the click function to fire.
This is old question, but some people, like me, who still see this issue, might want to read this: https://github.com/clauderic/react-sortable-hoc/issues/111#issuecomment-272746004
Issue is that sortable-hoc swallows onClick events as Matt found out. But we can have workarounds by setting pressDelay or distance.
For me the best option was to set minimum distance for sortable list and it worked nicely
You can also use the distance prop to set a minimum distance to be dragged before sorting is triggered (for instance, you could set a distance of 1px like so: distance={1})

Resources