Reputation: 539
I have a react/redux front-end that receives data via RabbitMQ. When a message arrives, our message handler will dispatch an action, e.g. PRODUCT_DETAILS_LOADED
, that updates the store and renders the appropriate component with the product details. This is 95% working, but if I try to display nested object properties, I get an error: Cannot read property <prop> of undefined
.
I have done some research on this. Most answers suggest that there's a race condition occurring, and that the component is trying to render the properties before they're "ready". I don't see how this can be the case, since I'm not making a REST call; I already have the data from the message queue and I've already updated the store. Moreover, a console.log inside the render
function will show the correct values of the nested properties. Here's the bulk of my ProductDetails component:
const mapStateToProps = state => {
return { product: state.productDetail };
}
class ProductDetails extends Component {
render() {
console.log(this.props.product.availability.forsale); //This will correctly show 'true' or 'false'
return (
<div>
<h3>Product Details Page</h3>
<h5>Details for {this.props.product.name}: </h5>
<div>
Name: {this.props.product.name}
<br/>
SKU: {this.props.product.sku}
<br/>
Available: {this.props.product.availability.forsale} //This throws an error
<br/>
{/*Price: {this.props.product.price.$numberDecimal} //This would also throw an error
<br/>*/}
Description: {this.props.product.description}
</div>
<Link to="/">Back to Home Page</Link>
</div>
)
}
}
const Details = connect(mapStateToProps, null)(ProductDetails);
export default Details;
Here's a snippet from the handler that fires when a product details message is received:
} else if(json.fields.routingKey === 'product.request.get') {
if(json.status === 'success') {
let payload = json.data;
store.dispatch(productDetailsLoaded(payload));
}
Here's the function that gets dispatches the action:
export function productDetailsLoaded(details) {
return { type: PRODUCT_DETAILS_LOADED, productDetail: details }
}
And finally, the reducer:
if(action.type === PRODUCT_DETAILS_LOADED) {
return Object.assign({}, state, {
productDetail: action.productDetail
});
}
I've seen some answers that suggest something like the following to prevent the error: let av = this.props.product.availability ? this.props.product.availability.forsale : '';
. This doesn't work for me. While it does prevent the error, I will display the blank value. This is not an option for me; I have to show correct information on the details page, not just "not-incorrect" information.
Can someone help me understand what's going on? Why does the console.log statement show the right data but the render
statement immediately after bomb? And how can I fix this?
Thanks!
EDIT: Here's the object I'm trying to render:
{
assets: {
imgs: "https://www.zzzzzzz.com/content/12626?v=1551215311"
}
availability: {
forsale: true,
expires: "2025-12-29T05:00:00.000Z"
}
createdAt: "2015-01-25T05:00:00.000Z"
description: "his is a fake description"
modifiedAt: "2019-05-28T04:00:00.000Z"
name: "Product Name"
price: {
$numberDecimal: "59.95"
}
sku: "ZZZZ"
"title ": "Product Title"
_id: "5ceec82aa686a03bccfa67cc"
}
Upvotes: 1
Views: 1462
Reputation: 1144
You are attempting to access the object before it is ready. The two places that you are having an error thrown are sub-properties. So for everything else, the initial value is "undefined" which your application is fine with, but once it tries to read a property of undefined, it will throw an error.
There are three solutions: 1) make sure the component does not load at all until the state has finished updating. This logic would be handled in the parent component of ProductDetails.
if 1) isn't an option, do one of the following:
2) set productDetails: null then in your component
render() {
console.log(this.props.product.availability.forsale); //This will correctly show 'true' or 'false'
if (!this.props.product) return <div />;
return (
// what you have above
)
}
}
const Details = connect(mapStateToProps, null)(ProductDetails);
export default Details;
3) Set a default state shape, something along the lines of:
{
assets: {
imgs: null
}
availability: {
forsale: null,
expires: null
}
createdAt: null
description: null
modifiedAt: null
name: null
price: {
$numberDecimal: null
}
sku: null
"title ": null
_id: null
}
This is part of the default behaviour of react as much as it is a redux issue. See https://reactjs.org/docs/react-component.html#setstate for more on how setState works and why you should not treat it as synchronous code.
Upvotes: 4