I am building a lazyloading component for images. But I have a problem with setting state. I'm getting Can only update a mounted or mounting component error, but I am using setState inside componentDidMount, which should allow me to avoid such errors.
Here's my code:
export default class Images extends React.Component {
constructor(props) {
super(props);
this.element = null;
this.state = {
loaded: false,
height: 0
};
}
componentDidMount() {
this.element = findDOMNode(this);
this.loadImage();
}
getDimensions() {
const w = this.element.offsetWidth;
let initw = 0;
let inith = 0;
let result;
const img = new Image();
img.src = this.props.url;
img.onload = (e) => {
initw = e.path[0].width;
inith = e.path[0].height;
result = (w / initw) * inith;
setTimeout(() => {
this.setState({
loaded: true,
height: `${result}px`
});
});
}
}
loadImage() {
_scrolling.add([this.element], () => {
if (this.element.classList.contains(_scrolling.classes.coming)) { // the image is visible
this.getDimensions();
}
});
}
render() {
const classes = this.state.loaded ? `${this.props.parentClass}__image--loaded` : null;
const styles = this.state.loaded ? {
maxHeight: this.state.height, minHeight: this.state.height, overflow: 'hidden'
} : null;
return (
<div className={`${this.props.parentClass}__image ${classes}`} style={styles}>
{this.state.loaded ?
<img
className={`${this.props.parentClass}__img`}
src={this.props.url}
title={this.props.title}
alt={this.props.title}
/>
: null
}
</div>
)
}
I belive the problem lies within img.onload, but I don't know how to achieve this otherwise. What should I do?
If you attempt to set state on an unmounted component, you’ll get an error like that.There are two solutions:
Assure Component isMounted : use setstate(); after checking that the component is mounted or not.
Abort the Request: When the component unmounts, we can just throw away the request so the callback is never invoked. To do this, we’ll take advantage of another React lifecycle hook, componentWillUnmount.
It seems that the img.onload handler is getting called on an unmounted Images component instance.
Image loading is asynchronous and takes some time. When it’s finally done and img.onload handler gets called, there is no guarantee your component is still mounted.
You have to make use of componentWillUnmount and make sure you either:
Cancel the image loading before component gets unmounted, or
Keep the track of the component’s mounted state and check if it’s mounted once your handler gets called
More about checking if a component is mounted or not: https://facebook.github.io/react/blog/2015/12/16/ismounted-antipattern.html
Solution: Cancel The Image Loading
export default class Images extends React.Component {
constructor(props) {
super(props);
this.element = null;
this.state = {
loaded: false,
height: 0
};
this.images = []; // We’ll store references to the Image objects here.
}
componentDidMount() {
this.element = findDOMNode(this);
this.loadImage();
}
componentWillUnmount() {
this.images.forEach(img => img.src = ''); // Cancel the loading of images.
}
getDimensions() {
const w = this.element.offsetWidth;
let initw = 0;
let inith = 0;
let result;
const img = new Image();
img.src = this.props.url;
img.onload = (e) => {
initw = e.path[0].width;
inith = e.path[0].height;
result = (w / initw) * inith;
setTimeout(() => {
this.setState({
loaded: true,
height: `${result}px`
});
});
}
this.images.push(img); // Store the reference.
}
loadImage() {
_scrolling.add([this.element], () => {
if (this.element.classList.contains(_scrolling.classes.coming)) { // the image is visible
this.getDimensions();
}
});
}
render() {
const classes = this.state.loaded ? `${this.props.parentClass}__image--loaded` : null;
const styles = this.state.loaded ? {
maxHeight: this.state.height, minHeight: this.state.height, overflow: 'hidden'
} : null;
return (
<div className={`${this.props.parentClass}__image ${classes}`} style={styles}>
{this.state.loaded ?
<img
className={`${this.props.parentClass}__img`}
src={this.props.url}
title={this.props.title}
alt={this.props.title}
/>
: null
}
</div>
)
}
}
I copied the image cancelling from: https://stackoverflow.com/a/5278475/594458
Related
I have a React Component, and a JSON which consists of image url, i pass the image url into image tag in my component through mapping function.
Here is a Example of my Code:
import imgDetails from "../data/ImageDetails";
class Example extends Component {
constructor(props) {
super(props);
this.state = {
imgContentLoad: false
}
}
imageHandleLoad() {
this.setState({
imgContentLoad: true
})
}
render() {
return ({
imgDetails.map((imgDetails) => {
return ( <
React.Fragment > {
(this.state.imgContentLoad &&
<
img src = {
imgDetails.imgURL
}
onLoad = {
this.imageHandleLoad.bind(this)
}
/>) || <
div > image is loading < /div>
} <
/React.Fragment>
)
}
)
}
Here i want to display the "image is loading" text to show until the image loads so i wrote the above image onload function code. but my problem is, "image is loading" is showing infinitely the image is not showing. what is wrong here?
extract it to a new component that will listen to the onload event and return the img when its loaded
class ImageWithLoading extends React.Component {
state = { isLoaded: false }
componentDidMount() {
const image = new Image();
image.onload = () => this.setState({ isLoaded: true });
image.src = this.props.src;
}
render() {
const { src } = this.props;
const { isLoaded } = this.state;
return isLoaded
? <img src={src} />
: <div>Loading image...</div>
}
}
Then make the Example component just a container that maps the data to the ImageWithLoading component
const Example = () => imgDetails
.map(({ imgURL }) => <ImageWithLoading key={imgURL} src={imgURL} />);
I have an Image class which allows me to change images from the containiner Component and update the image style.
My Class:
import React from "react";
import Radium from 'radium';
class StateImage extends React.Component {
constructor(props) {
super(props);
this.state = {
images: this.props.images.map(image => ({
...image,
loaded: false,
activeStyle: {visibility: 'hidden'}
})),
activeMode: props.activeMode
};
this.state.images.forEach((image, index) => {
const src = image.image;
const primaryImage = new Image();
primaryImage.onload = () => {
const images = [...this.state.images];
images[index].loaded = true;
if (images[index].name === this.state.activeMode) {
images[index].activeStyle = images[index].style;
// is this image the default activated one? if so, activate it now that it's loaded.
images[index].onActivate();
} else
images[index].activeStyle = {visibility: 'hidden'};
this.setState( {
...this.state,
images
});
};
primaryImage.src = src;
});
}
updateImageStyle = (name, style) => {
let images = [...this.state.images].map( (image) => {
if (image.name === name) {
return {
...image,
style: style,
activeStyle: style
}
} else return image;
});
this.setState({
...this.state,
images: images
}, () => {
console.log("updated state");
console.log(this.state);
});
};
onClick = () => {
this.state.images.map( (image) => {
if (image.clickable && image.name === this.state.activeMode)
this.props.eventHandler(this.state.activeMode);
});
};
render () {
console.log("render");
console.log(this.state.images);
let images = this.state.images.map((image, index) => {
return <img
key={ index }
onClick={ this.onClick }
style={ image.activeStyle }
src={ image.image }
alt={ image.alt}/>
});
return (
<div>
{images}
</div>
);
}
}
export default Radium(StateImage);
My problem revolves around updateImageStyle. When this function is called I need to change the style element of the active image and re-render so that users see the change.
updateImageStyle is reached, and I update the images portion of my state. I console.log it once the setState is done and I can verify the change was made correctly!
However, I also console.log from the render and to my amazement, the this.state.images outputted from render is stale and does not reflect my changes.
How can this be? the console.log proves the render that has the stale state is called AFTER I have confirmed my changes have taken place.
My console log:
You are most likely seeing your state being overwritten by a different setState call perhaps the one in primaryImage.onload. Since React batches setState calls together, render() is called only once with the updates.
I'm setting up a virtualized scrolling component in React, and using refs with a recycled observer to notify the app when to prepare another batch of data. Inside my Grid component, I map over the current batch of data and assign a ref to a sentinel div, except that ref returns null in componentDidMount(). I don't understand why since componentDidMount fires after render executes, so the reference should be available.
The only workaround to this I've found is using this janky solution in my componentDidMount: setTimeout(() => this.observer.observe(this.targetRef.current), 0);.
import React, { Component, createRef } from "react";
class Grid extends Component {
constructor(props) {
super(props);
this.state = {
batch: []
};
this.observer = null;
this.targetRef = null;
this.lastRowFirstVisible =
props.rowCount * props.columnCount - props.columnCount;
this.config = {
rootMargin: "0px",
threshold: 1
};
this.setTargetRef = element => {
this.targetRef = element;
};
}
componentDidMount() {
const { startIndex, numberToDisplay } = this.props;
this.setBatch(startIndex, numberToDisplay);
this.observer = new IntersectionObserver(function(entries, self) {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log(entry);
// self.unobserve(entry.target);
}
});
}, this.config);
setTimeout(() => this.observer.observe(this.targetRef), 0);
}
setBatch = (startIndex, numberToDisplay) => {
const batch = this.getBatch(startIndex, numberToDisplay);
this.setState({ batch });
};
getBatch = (startIndex, numberToDisplay) => {
const { data } = this.props;
return data.slice(startIndex, numberToDisplay);
};
// TO DO
updateObserver = () => {
this.observer.observe(this.targetRef.current);
};
render() {
const { lastRowFirstVisible } = this;
const { batch } = this.state;
const { elementWidth, elementHeight } = this.props;
console.log(lastRowFirstVisible);
return (
<>
{batch.map((element, localIndex) => {
const { index } = element;
console.log(localIndex === lastRowFirstVisible);
return localIndex === lastRowFirstVisible ? (
<div
id={index}
key={index}
style={{ width: elementWidth, height: elementHeight }}
className="card"
ref={this.setTargetRef}
>
{this.props.renderRow(element)}
</div>
) : (
<div
key={index}
style={{ width: elementWidth, height: elementHeight }}
className="card"
>
{this.props.renderRow(element)}
</div>
);
})}
</>
);
}
}
export default Grid;
Expected results: render function finishes executing, assigns DOM node to this.targetRef for use in componentDidMount()
Actual results: this.targetRef is still null in componentDidMount()
I am getting the warning below when I force update on a grid like this:
proxyConsole.js:56 Warning: forceUpdate(...): Cannot update during an existing state transition (such as within `render` or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to `componentWillMount`.
Not sure how to amend where to put forceUpdate() so that it behaves correctly.
const quoteList = [];
class App extends Component {
constructor() {
super();
this.state = {
response: {},
endpoint: "http://127.0.0.1:4001"
};
}
componentDidMount() {
const { endpoint } = this.state;
const socket = socketIOClient(endpoint);
this.theGrid.forceUpdate();
socket.on("FromAPI", data => this.setState({ response: data }));
}
cellRenderer ({ columnIndex, key, rowIndex, style }) {
return (
<div
key={key}
style={style}
>
{quoteList[rowIndex][columnIndex]}
</div>
)
}
render() {
const { response } = this.state;
if(Object.keys(response).length !== 0)
{
//console.log(response);
const row = symbolRowDictionary[response.ticker];
var column = 0;
switch(response.type)
{
case 'bid':
column = 1;
break;
case 'ask':
column = 3;
break;
case 'last':
column = 5;
break;
default:
console.log('Unknown type');
}
quoteList[row][column] = response.size;
quoteList[row][column + 1] = response.price;
this.theGrid.forceUpdate();
}
return (
<div style={{ textAlign: "center" }}>
{
<ul><li>Quote: {response.ticker} {response.type} {response.price} {response.size}</li></ul>
}
<div>
</div>
<Grid
ref={(ref) => this.theGrid = ref}
cellRenderer={this.cellRenderer}
columnCount={7}
columnWidth={75}
height={quoteList.length * 20}
rowCount={quoteList.length}
rowHeight={20}
width={800}
/>
</div>
);
}
}
export default App;
I would move quoteList to component state as following:
const endpoint: "http://127.0.0.1:4001";
class App extends Component {
constructor() {
super();
this.state = {
response: {},
quoteList = [];
};
}
componentDidMount() {
const socket = socketIOClient(endpoint);
let _this = this;
let {quoteList} = this.state;
socket.on("FromAPI", data => {
if(Object.keys(response).length !== 0) {
const row = symbolRowDictionary[response.ticker];
var column = 0;
switch(response.type)
{
case 'bid':
column = 1;
break;
case 'ask':
column = 3;
break;
case 'last':
column = 5;
break;
default:
console.log('Unknown type');
}
quoteList[row][column] = response.size;
quoteList[row][column + 1] = response.price;
_this.setState({ response: data, quoteList: quoteList })
}
}));
}
cellRenderer ({ columnIndex, key, rowIndex, style }) {
let {quoteList} = this.state;
return (
<div
key={key}
style={style}
>
{quoteList[rowIndex][columnIndex]}
</div>
)
}
render() {
const { response, quoteList } = this.state;
return (
<div style={{ textAlign: "center" }}>
{
<ul><li>Quote: {response.ticker} {response.type} {response.price} {response.size}</li></ul>
}
<div>
</div>
<Grid
ref={(ref) => this.theGrid = ref}
cellRenderer={this.cellRenderer}
columnCount={7}
columnWidth={75}
height={quoteList.length * 20}
rowCount={quoteList.length}
rowHeight={20}
width={800}
/>
</div>
);
}
}
export default App;
When quoteList is updated, its length will be updated accordingly and these changes will be passed to the child component Grid
EDIT
As you do not understand why does this code run, I will explan a little bit. If you don't know about React lifecycle, please take a look at this illustration
quoteList is initialized as an empty array in constructor
render is called the first time. cellRenderer will be bound to this component, or this point to App component.
After component is mounted, componentDidMount will be triggered. Here is when we should fetch data from API. Within the function block (data => {...}) , this is pointing to the function itself. So before fetching the API, I have to assign _this = this to be able to call setState.
setState is executed, render will be triggered again and since quoteList is changed. Your App will be rendered differently according to new quoteList. Means the new quoteList now is passed to your Grid. Grid also re-render.
With this flow, you don't need to forceUpdate.
After much experimentation and some online docs, I was able to solve it by putting this.theGrid.forceUpdate like this. There are no warnings, and render and rendercell are kept "pure"
componentDidMount() {
const socket = socketIOClient(endpoint);
socket.on("FromAPI", data =>
{
this.setState({ response: data });
this.theGrid.forceUpdate();
}
);
}
I try to add hammer js to my reactjs component and my component looks as it follows
import React from 'react';
import _ from 'underscore';
import Hammer from 'hammerjs';
class Slider extends React.Component {
constructor(props) {
super(props)
this.updatePosition = this.updatePosition.bind(this);
this.next = this.next.bind(this);
this.prev = this.prev.bind(this);
this.state = {
images: [],
slidesLength: null,
currentPosition: 0,
slideTransform: 0,
interval: null
};
}
next() {
console.log('swipe')
const currentPosition = this.updatePosition(this.state.currentPosition - 10);
this.setState({ currentPosition });
}
prev() {
if( this.state.currentPosition !== 0) {
const currentPosition = this.updatePosition(this.state.currentPosition + 10);
this.setState({currentPosition});
}
}
componentDidMount() {
this.hammer = Hammer(this._slider)
this.hammer.on('swipeleft', this.next());
this.hammer.on('swiperight', this.prev());
}
componentWillUnmount() {
this.hammer.off('swipeleft', this.next())
this.hammer.off('swiperight', this.prev())
}
handleSwipe(){
console.log('swipe')
}
scrollToSlide() {
}
updatePosition(nextPosition) {
const { visibleItems, currentPosition } = this.state;
return nextPosition;
}
render() {
let {slides, columns} = this.props
let {currentPosition} = this.state
let sliderNavigation = null
let slider = _.map(slides, function (slide) {
let Background = slide.featured_image_url.full;
if(slide.status === 'publish')
return <div className="slide" id={slide.id} key={slide.id}><div className="Img" style={{ backgroundImage: `url(${Background})` }} data-src={slide.featured_image_url.full}></div></div>
});
if(slides.length > 1 ) {
sliderNavigation = <ul className="slider__navigation">
<li data-slide="prev" className="" onClick={this.prev}>previous</li>
<li data-slide="next" className="" onClick={this.next}>next</li>
</ul>
}
return <div ref={
(el) => this._slider = el
} className="slider-attached"
data-navigation="true"
data-columns={columns}
data-dimensions="auto"
data-slides={slides.length}>
<div className="slides" style={{ transform: `translate(${currentPosition}%, 0px)`, left : 0 }}> {slider} </div>
{sliderNavigation}
</div>
}
}
export default Slider;
the problem is like on tap none of the components method are fired.
How do I deal in this case with the hammer js events in componentDidMount
Reason is, inside componentDidMount lifecycle method swipeleft and swiperight expect the functions but you are assigning value by calling those methods by using () with function name. Remove () it should work.
Write it like this:
componentDidMount() {
this.hammer = Hammer(this._slider)
this.hammer.on('swipeleft', this.next); // remove ()
this.hammer.on('swiperight', this.prev); // remove ()
}