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.
Related
I have a parent class component called CardView.js which contains a child class component called Tab.js (which contains a FlatList).
When a button is pressed in CardView.js, a modal appears with various options. A user chooses an option and presses 'OK' on the modal. At this point the onOKHandler method in the parent component updates the parent state (tabOrderBy: orderBy and orderSetByModal: true). NOTE: These two pieces of state are passed to the child component as props.
Here is what I need:
When the onOKHandler is pressed in the parent, I need the child component to re-render with it's props values reflecting the new state values in the parent state. NOTE: I do not want the Parent Component to re-render as well.
At the moment when onOKHandler is pressed, the child component reloads, but it's props are still showing the OLD state from the parent.
Here is what I have tried:
When the onOKHandler is pressed, I use setState to update the parent state and then I use the setState callback to call a method in the child to reload the child. The child reloads but its props are not updated.
I have tried using componentDidUpdate in the child which checks when the prop orderSetByModal is changed. This does not work at all.
I have tried many of the recommendations in other posts like this - nothing works! Where am I going wrong please? Code is below:
Parent Component: CardView.js
import React from "react";
import { View } from "react-native";
import { Windows} from "../stores";
import { TabView, SceneMap } from "react-native-tab-view";
import { Tab, TabBar, Sortby } from "../components";
class CardView extends React.Component {
state = {
level: 0,
tabIndex: 0,
tabRoutes: [],
recordId: null,
renderScene: () => {},
showSortby: false,
orderSetByModal: false,
tabOrderBy: ''
};
tabRefs = {};
componentDidMount = () => {
this.reload(this.props.windowId, null, this.state.level, this.state.tabIndex);
};
reload = (windowId, recordId, level, tabIndex) => {
this.setState({ recordId, level, tabIndex });
const tabRoutes = Windows.getTabRoutes(windowId, level);
this.setState({ tabRoutes });
const sceneMap = {};
this.setState({ renderScene: SceneMap(sceneMap)});
for (let i = 0; i < tabRoutes.length; i++) {
const tabRoute = tabRoutes[i];
sceneMap[tabRoute.key] = () => {
return (
<Tab
onRef={(ref) => (this.child = ref)}
ref={(tab) => (this.tabRefs[i] = tab)}
windowId={windowId}
tabSequence={tabRoute.key}
tabLevel={level}
tabKey={tabRoute.key}
recordId={recordId}
orderSetByModal={this.state.orderSetByModal}
tabOrderBy={this.state.tabOrderBy}
></Tab>
);
};
}
};
startSortByHandler = () => {
this.setState({showSortby: true});
};
endSortByHandler = () => {
this.setState({ showSortby: false});
};
orderByFromModal = () => {
return 'creationDate asc'
}
refreshTab = () => {
this.orderByFromModal();
this.child.refresh()
}
onOKHandler = () => {
this.endSortByHandler();
const orderBy = this.orderByFromModal();
this.setState({
tabOrderBy: orderBy,
orderSetByModal: true}, () => {
this.refreshTab()
});
}
render() {
return (
<View>
<TabView
navigationState={{index: this.state.tabIndex, routes: this.state.tabRoutes}}
renderScene={this.state.renderScene}
onIndexChange={(index) => {
this.setState({ tabIndex: index });
}}
lazy
swipeEnabled={false}
renderTabBar={(props) => <TabBar {...props} />}
/>
<Sortby
visible={this.state.showSortby}
onCancel={this.endSortByHandler}
onOK={this.onOKHandler}
></Sortby>
</View>
);
}
}
export default CardView;
Child Component: Tab.js
import React from "react";
import { FlatList } from "react-native";
import { Windows } from "../stores";
import SwipeableCard from "./SwipeableCard";
class Tab extends React.Component {
constructor(props) {
super(props);
this.state = {
currentTab: null,
records: [],
refreshing: false,
};
this.listRef = null;
}
async componentDidMount() {
this.props.onRef(this);
await this.reload(this.props.recordId, this.props.tabLevel, this.props.tabSequence);
}
componentWillUnmount() {
this.props.onRef(null);
}
//I tried adding componentDidUpdate, but it did not work at all
componentDidUpdate(prevProps) {
if (this.props.orderSetByModal !== prevProps.orderSetByModal) {
this.refresh();
}
}
getOrderBy = () => {
let orderByClause;
if (this.props.orderSetByModal) {
orderByClause = this.props.tabOrderBy;
} else {
orderByClause = "organization desc";
}
return orderByClause;
};
async reload() {
const currentTab = Windows.getTab(this.props.windowId, this.props.tabSequence, this.props.tabLevel);
this.setState({ currentTab });
let response = null;
const orderBy = this.getOrderBy();
response = await this.props.entity.api.obtainRange(orderBy);
this.setState({ records: response.dataList })
}
refresh = () => {
this.setState({ refreshing: true }, () => {
this.reload(this.props.recordId, this.props.tabLevel, this.props.tabSequence)
.then(() => this.setState({ refreshing: false }));
});
};
renderTabItem = ({ item, index }) => (
<SwipeableCard
title={"Card"}
/>
);
render() {
if (!this.state.currentTab) {
return null;
}
return (
<>
<FlatList
ref={(ref) => (this.listRef = ref)}
style={{ paddingTop: 8 }}
refreshing={this.state.refreshing}
onRefresh={this.refresh}
data={this.state.records}
keyExtractor={(item) => (item.isNew ? "new" : item.id)}
/>
</>
);
}
}
export default Tab;
Suppose that I have a Modal inside a Component and I want to show the Modal when some buttons are clicked:
render(){
...
<Modal
is={this.state.productId}
visilble={this.state.visible}
/>
}
Inside the Modal Component, I want to call API to get the product detail based on the selected id as the following:
componentWillReceiveProps(nextProps) {
if(nextProps.visible && !this.props.visible) {
fetch(...).then(res => {
this.setState({
product: res.data
})
}).catch(err => {
...
})
}
}
From the React docs it says that componentWillReceiveProps, componentWillUpdate is deprecated and you should avoid them in new code.So I try use static getDerivedStateFromProps()
static getDerivedStateFromProps()(nextProps) {
if(nextProps.visible && ...) {
fetch(...).then(res => {
return {
product: res.data
}
}).catch(err => {
...
})
} else return null (or just return null here without else)
}
The above code doesn't work since fetch is asynchronous so it always returns null or doesn't return anything, you can't use await here to wait for the api to resolve also, and I heard that getDerivedStateFromProps shouldn't use for data fetching.
So what is the best way solve the problem ?
I think it's better to decide whether to show Modal component or not in parent component as Modal component should be a functional component to render only modal related view. This way every time Modal component will not be rendered and only rendered when visible flag is true.
{ this.state.visible &&
<Modal />
}
In parent component you could fetch data in componentDidMount if just after initial render the data is required or componentDidUpdate if after every update the fetch data is required for modal. After fetching data set state of visible to true.
Happy Coding!!!
You can mount the Modal based on this.state.visible and start fetching when Modal is mounted on componentDidMount or when the props are changing through componentDidUpdate
// delay
const delay = () => new Promise(res => setTimeout(res, 1000));
// fake products
const products = [
{ id: 1, text: "product 1" },
{ id: 2, text: "product 2" },
{ id: 3, text: "product 3" }
];
// fake ajax call
const API = async productId => {
await delay();
return products.find(p => p.id === productId);
};
class Parent extends Component {
state = { productId: 1, visible: false };
toggleShow = () => {
this.setState(prevState => ({ visible: !prevState.visible }));
};
setProductId = productId => this.setState({ productId, visible: true });
render() {
return (
<div className="App">
<button onClick={this.toggleShow}>show or hide</button>
<button onClick={() => this.setProductId(1)}>fetch product 1</button>
<button onClick={() => this.setProductId(2)}>fetch product 2</button>
<button onClick={() => this.setProductId(3)}>fetch product 3</button>
<button onClick={() => this.setProductId(4)}>unknown product id</button>
{this.state.visible && <Modal is={this.state.productId} />}
</div>
);
}
}
class Modal extends Component {
state = { product: null, fetching: false };
componentDidMount = () => {
this.fetchProduct();
};
componentDidUpdate = prevProps => {
if (prevProps.is !== this.props.is) {
this.fetchProduct();
}
};
fetchProduct = async () => {
this.setState({ fetching: true });
const product = await API(this.props.is);
this.setState({ product, fetching: false });
};
render() {
const { product, fetching } = this.state;
if (fetching) return <h1>{`fetching product ${this.props.is}`}</h1>;
return product ? (
<div>
<h1>{`product id: ${product.id}`}</h1>
<h3>{`text: ${product.text}`}</h3>
</div>
) : (
<h1>Product not found</h1>
);
}
}
SandBox
Actually in another approach, you can use <modal/> in another pureComponent
(for example: componentContainer) and just call it in your main view. and use just one object inside your main view state as your <componentContainer/> data scope and give it as a property like this code
construcor(prop){
super(props);
this.state={
productDetail:null<<change this in data fetch
}
}
<componentContainer data={this.state.productDetail} modalVisible={this.state.changeNumderModal}/>
and inside your component container:
<Modal
animationType="fade"
transparent={true}
visible={this.props.modalVisible}//<<<<<<<<<
onRequestClose={() => {
}}>
//contents:
this.props.data.map(()=>//-----)
</View>
</Modal>
in this method you have just one modal and one scope as its data, you can call the fetchs and other functions in your main component as others... and if its so needed you can even pass a function as property to modal container to :
someFunction(someval){
//do some thing
}
<componentContainer data={this.state.productDetail} modalVisible={this.state.changeNumderModal} someFunction={(val)=>this.someFunction(val)}/>
and call it inside :
this.props.someFunction('what ever you want')
So Let's assume you did that hide and show of modal with button click, now whenever the model will open
inside the componentWillMount function
class Modal extends Component {
this.state = {product:[], loader : false}
componentWillMount(){
fetch(...).then(res => {
this.setState({
product: res.data
})
}).catch(err => {
...
})
}
render(){
const {product, loader} = this.state;
return (
if (loader) return <ProductComponent data = {product}/>
else return <div> No data found </div>
);
}
}
You can use the approach below:
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.props.visible && !prevProps.visible) {
// make the API call here
}
}
I have a container component which gets page number as prop and downloads data for that page. I rely on componentDidUpdate() to trigger the download as componentDidUpdate() fires when pageNumber changes. Is this a reasonable way to do it?
One thing I noticed is that the component gets re-rendered when it receives a new pageNumber even though nothing changes at first and then it re-renders again once data has been downloaded. The first re-render is superfluous. Should I not be bothered by this?
If I was really bothered, I could user shouldComponentUpdate() to only re-render when data changes. (I wonder if this check might even be more costly than the re-render itself?) However, if I used shouldComponentUpdate() and not update on page change, then I couldn't rely on componentDidUpdate() to load my data anymore.
Does this mean that the below is the way to do it or is there a better way?
import React from 'react';
import PropTypes from 'prop-types';
import Table from "../components/Table";
import Pagination from "../components/Pagination";
import {connect} from "react-redux";
import {changePage} from "../js/actions";
const PAGE_COUNT = 10;
const mapStateToProps = state => {
return { currentPage: state.currentPage }
};
const mapDispatchToProps = dispatch => {
return {
changePage: page => dispatch(changePage(page))
};
};
class ConnectedTableContainer extends React.Component {
state = {
data: [],
loaded: false,
};
handlePageChange = page => {
if (page < 1 || page > PAGE_COUNT) return;
this.props.changePage(page);
};
loadData = () => {
this.setState({ loaded: false });
const { currentPage } = this.props;
const pageParam = currentPage ? "?_page=" + currentPage : "";
fetch('https://jsonplaceholder.typicode.com/posts/' + pageParam)
.then(response => {
if (response.status !== 200) {
console.log("Unexpected response: " + response.status);
return;
}
return response.json();
})
.then(data => this.setState({
data: data,
loaded: true,
}))
};
componentDidMount() {
this.loadData(this.props.currentPage);
}
componentDidUpdate(prevProps) {
if (prevProps.currentPage != this.props.currentPage) {
this.loadData();
}
}
render() {
const { loaded } = this.state;
const { currentPage } = this.props;
return (
<div className="container">
<div className="section">
<Pagination onPageChange={ this.handlePageChange } pageCount={ PAGE_COUNT } currentPage={ currentPage }/>
</div>
<div className={ "section " + (loaded ? "" : "loading") }>
<Table data={ this.state.data } />
</div>
</div>
)
}
}
ConnectedTableContainer.propTypes = {
changePage: PropTypes.func.isRequired,
currentPage: PropTypes.number.isRequired,
};
ConnectedTableContainer.defaultProps = {
currentPage: 1,
};
const TableContainer = connect(mapStateToProps, mapDispatchToProps)(ConnectedTableContainer);
export default TableContainer;
It is perfectly fine to use componentDidUpdate() to trigger the download when pageNumber changes.
I would not recommend to implement shouldComponentUpdate, instead inherit from React.PureComponent. This implements shouldComponentUpdate for you by comparing props and state. If any of the props and state change (shallow comparison), it'll re-render, otherwise not.
I try to animate a div with reactjs using async data via redux and it's not clear to me when can I have a reference to the virtual dom on state loaded.
In my case I have a div with id header where I would like to push down the container when data was populated.
If I try in componentDidMount than I get Cannot read property 'style' of undefined because componentDidMount still having a reference to an on load container
class HomePage extends React.Component {
constructor(props) {
super(props);
this.state = {
sliderLength: null
}
}
componentDidMount() {
this.props.actions.getSlides()
if(this.header) {
setTimeout(function() {
this.header.style.bottom = -(this.header.clientHeight - 40) + 'px';
}, 2000);
}
//header.style.bottom = -pushBottom+'px';
}
componentWillReceiveProps(nextProps) {
let {loaded} = nextProps
if(loaded === true ) {
this.animateHeader()
}
}
animateHeader() {
}
componentWillMount() {
const {slides} = this.props;
this.setState({
sliderLength: slides.length,
slides: slides
});
}
render() {
const {slides, post, loaded} = this.props;
if(loaded ===true ) {
let sliderTeaser = _.map(slides, function (slide) {
if(slide.status === 'publish') {
return <Link key={slide.id} to={'portfolio/' + slide.slug}><img key={slide.id} className="Img__Teaser" src={slide.featured_image_url.full} /></Link>
}
});
let about = _.map(post, function (data) {
return data.content.rendered;
})
return (
<div className="homePage">
<Slider columns={1} autoplay={true} post={post} slides={slides} />
<div id="header" ref={ (header) => this.header = header}>
<div className="title">Title</div>
<div className="text-content">
<div dangerouslySetInnerHTML={createMarkup(about)}/>
</div>
<div className="sliderTeaser">
{sliderTeaser}
</div>
<div className="columns">
<div className="column"></div>
<div className="column"></div>
<div className="column"></div>
</div>
</div>
<div id="bgHover"></div>
</div>
);
} else {
return <div>...Loading</div>
}
}
}
function mapStateToProps(state) {
return {
slides: state.slides,
post: state.post,
loaded: state.loaded
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(slidesActions, dispatch)
};
}
function createMarkup(markup) {
return {__html: markup};
}
export default connect(mapStateToProps, mapDispatchToProps)(HomePage);
How do I deal in this case with states?
Between I found a solution but not sure if is the right workaround
componentDidUpdate() {
if(this.header) {
setTimeout(function() {
this.header.style.bottom = -(this.header.clientHeight - 35) + 'px';
}, 2000);
}
}
In general, try to avoid using ref as much as possible. This is particularly difficult if you're new to React but with some training, you'll find yourself not needing it.
The problem with modifying the styles like you're doing is that when the component will render again your changes will be overwritten.
I would create a new state property, say state.isHeaderOpen. In your render method you will render the header differently depending on the value of this header e.g.:
render () {
const {isHeaderOpen} = this.state
return (
<header style={{bottom: isHeaderOpen ? 0 : 'calc(100% - 40px)'}}>
)
}
Here I'm using calc with percentage values to get the full height of the header.
Next, in your componentDidMount simply update the state:
componentDidMount () {
setTimeout(() => this.setState({isHeaderOpen: false}), 2000);
}
In this way, the component will render again but with the updated style.
Another way is to check if the data has been loaded instead of creating a new state value. For example, say you're loading a list of users, in render you would write const isHeaderOpen = this.state.users != null.
If you are trying to animate a div why are you trying to access it by this.header just use the javaScript's plain old document.getElementById('header') and then you can play around with the div.
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