Reputation: 15647
The Scenario:
I have a responsive div
container (CSS - Width : 33%) with child images. Not wanting the user to scroll down, I want to find out the absolute dimensions of the div
, so that I can calculate the number of images (which have fixed Width & Height) which can fit in. Then to render only the images which can fit on the screen.
I am using React & Redux for this app. Here is the logic I was thinking of, which does NOT work.
_ Smart Component (subscribed to the Store)
|___ `div` container (presentational component)
|______ images (presentational components)
I render the 'presentational' component, then in the componentDidMount
, I dispatch a function which finds out the Width & Height of the div
container & updates the Redux state with 'image capacity' value (how many images can be stored in the div
container).
Immediately after the above function, while still in componentDidMount
, I dispatch another function which should take the UPDATED values from the store (image capacity) & update the store with image data.
The idea being that when the store is updated, the smart component will re-render the presentational components (div
& images) & thus a second round of re-rendering would occur to display the images.
Unfortunately, this is flawed. The props being passed to the presentational components are 'pre-update' store values (0) so when I run the dispatch the second function to update the store with images, the 'image capacity' value it has in its props is still zero (0).
I wonder what correct logic to implement here?
In case, the code makes more sense, here is the presentational component code:
const PreviewTemParent = React.createClass({
componentDidMount : function() {
let elePreviewParent = ReactDOM.findDOMNode( this.refs.previewParent );
previewTemParentAttr.width = elePreviewParent.clientWidth;
previewTemParentAttr.height = elePreviewParent.clientHeight;
this.props.findImgCapacity();
this.temImgToShow( "body" );
},
temImgToShow : function( templateType ) {
let pagination = this.props.previewTemState.get( "pagination" );
let imgCapacity = this.props.previewTemState.get( "imgCapacity" );
console.log( "%c In `PreviewTemParent`, pag, imgCap & props are...", "color : gold", pagination, "; ", imgCapacity, "; ", this.props );
this.props.temImgToShow( templateType, pagination, imgCapacity );
},
render : function() {
return(
<div
className = "previewParent"
ref = "previewParent">
<div className = "previewContainer">
{ this.props.previewTemState.get( "temImg" ).map( ( imgObj ) => {
<PreviewTemImgContainer data = { imgObj } />;
} ) }
</div>
</div>
);
}
});
Upon @pierrepinard_2's request, Im posting a detailed code, hopefully explaining what I have tried.
I tried a few things, including adding a 'smart component' to the parent of <PreviewTemParent>
& then getting the parent to calculate the dimensions. However, in that case the props are still not updated, understandably because the componentDidMount is run after <PreviewTemParent>
has been mounted.
Here is the solution I tried as per suggestion from @pierrepinard_2 regarding using componentWillReceiveProps
, which doesnt seem to be called.
C_PreviewTemParent CONTAINER (SMART COMPONENT)
import { connect } from "react-redux";
import { temImgToDisplayInContainer } from "../modcon/Actions.js";
import PreviewTemParent from "../component/temContainer/PreviewTemParent.jsx";
const mapStateToProps = ({ previewTemImgState }) => {
return({
previewTemState : previewTemImgState
});
};
const mapDispatchToProps = ( dispatch ) => {
return({
temImgToDisplayInContainer : ( templateType, activePage ) => {
dispatch( temImgToDisplayInContainer( templateType, activePage ) );
}
});
};
export const C_PreviewTemParent = connect( mapStateToProps, mapDispatchToProps )( PreviewTemParent );
PreviewParent COMPONENT
const PreviewTemParent = React.createClass({
componentDidMount : function() {
this.props.temImgToDisplayInContainer( "body" );
},
componentWillReceiveProps : function( nextProps ) {
console.log( "%c componentWillReceiveProps is...", "color: green", nextProps );
},
render : function() {
return(
<div className = "previewParent">
<div className = "previewContainer">
{ this.props.previewTemState.get( "temImg" ).map( ( imgObj ) => {
<PreviewTemImgContainer data = { imgObj } />;
} ) }
</div>
</div>
);
}
});
ACTION CREATOR Please excuse the lengthy code :!
export const temImgToDisplayInContainer = ( templateType, activePage ) => {
// temImgCapacity finds out the number of images that can
// fit in the container based on the container's dynamic Width & Height.
// in order to make the func reusable, activePage is an optional parameter
// hence the `if`
let temImgCapacity;
if( activePage ){
temImgCapacity = findImgCapacityInPreviewTem( false );
} else {
temImgCapacity = findImgCapacityInPreviewTem( true );
}
// `previewTemImgData` is a store of static data of all the images.
// during development, this is being used instead of ajax calls etc
// `temImgData` stores the data only for the 'templates' we are interested in
let temImgData = previewTemImgData.get( templateType );
let counter = 1;
// `noOfTemplate` is how many images we will render
let noOfTemplate = temImgCapacity.get( "imgCapacity" );
let currentPage = activePage || temImgCapacity.get( "activePage" );
// `maxCounter` is for pagination, which is the last template to store
let maxCounter = currentPage * noOfTemplate;
let temStartFrom = (( currentPage * noOfTemplate ) - noOfTemplate ); // what template start number.
// get the tempales we are only interested in
let filteredTemImgData = temImgData.filter( ( templateObj ) => {
if (( counter <= maxCounter ) && ( counter >= temStartFrom )) {
counter++;
return( templateObj );
}
} );
// merging the data from above first line 'temImgCapacity' & the filtered
// temImgData
let payloadData = filteredTemImgData.merge(( temImgCapacity ));
return({
type : CURRENT_TEM_IMG,
payload : payloadData
});
};
Finally, THE REDUCER:
const previewTemImgState = ( state = initialPreviewTemState, action ) => {
switch( action.type ) {
case( FIND_IMG_CAPACITY_IN_PREVIEW_TEM ) :
return(
state.set( "imgCapacity", action.payload.imgCapacity )
);
case( RENDER_TEM_IMG ) :
console.log( action.payload );
var newState = state.set( "temImg", action.payload.temImg );
console.log( "%c previewTemImgState is...", "color : red", state.get( "temImg" ), " ; ", newState.get( "temImg" ) );
return(
state.set( "temImg", action.payload )
);
default :
return state;
}
};
Upvotes: 2
Views: 3404
Reputation: 1426
The problem is that when you call this.props.previewTemState
in temImgToShow()
, the state object reference is still the same as in the beginning of componentDidMount()
execution, before this.props.findImgCapacity()
was called.
One option would be to trigger only one synthetic action from componentDidMount()
, with following arguments: width and height (to calculate the image capacity), templateType, pagination. Then you would only get one UI update with desired state.
If you really cannot trigger only one action to perform all the work, then you could fire the second action in componentWillReceiveProps()
as Honza Haering suggested in comments:
componentDidMount: function() {
let elePreviewParent = ReactDOM.findDOMNode( this.refs.previewParent );
previewTemParentAttr.width = elePreviewParent.clientWidth;
previewTemParentAttr.height = elePreviewParent.clientHeight;
this.props.findImgCapacity();
},
componentWillReceiveProps: function(nextProps) {
let newImgCapacity = nextProps.previewTemState.get( "imgCapacity" );
let oldImgCapacity = this.props.previewTemState.get( "imgCapacity" );
if (newImgCapacity !== oldImgCapacity && newImgCapacity > 0) {
let pagination = nextProps.previewTemState.get( "pagination" );
nextProps.temImgToShow("body", pagination, newImgCapacity);
}
},
Or, if you use redux-thunk middleware, you could use async action creators with promises to properly chain actions. If you give us the code of your actions, I could show you how to do it.
Upvotes: 2