I have a redux component that is parsing the JSON (at bottom), but I can't figure out how to grab the nested child objects. I don't think I'm understanding correctly how mapStateToProps works.
The console log is dumping the child objects, but when I try to access services.name I get
"Cannot read property 'name' of undefined"
Can someone help me understand how to map properties here? I've included an example of the JSON I'm grabbing from the API at the bottom.
services-list.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actions from '../actions';
class ServicesList extends Component {
componentDidMount(){
this.props.fetchServices();
}
render() {
//console.log('render called in ServicesList component');
return (
<table className='table table-hover'>
<thead>
<tr>
<th>Service Name</th>
</tr>
</thead>
<tbody>
{this.props.services.map(this.renderServices)}
</tbody>
</table>
);
}
renderServices(data) {
console.log(data.services);
const name = data.services.name;
return(
<tr key={name}>
<td>{name}</td>
</tr>
);
}
}
function mapStateToProps({services}) {
return { services };
}
export default connect(mapStateToProps, actions)(ServicesList);
My JSON looks like this:
{
"services": [
{
"name": "redemption-service",
"versions": {
"desired": "20170922_145033",
"deployed": "20170922_145033"
},
"version": "20170922_145033"
}, {
"name": "client-service",
"versions": {
"desired": "20170608_094954",
"deployed": "20170608_094954"
},
"version": "20170608_094954"
}, {
"name": "content-rules-service",
"versions": {
"desired": "20170922_130454",
"deployed": "20170922_130454"
},
"version": "20170922_130454"
}
]
}
Finally, I have an action that exposes the axios.get here:
import axios from 'axios';
const ROOT_URL=`http://localhost:8080/services.json`;
export const FETCH_SERVICES = 'FETCH_SERVICES';
export function fetchServices(){
const url = `${ROOT_URL}`;
const request = axios.get(url);
return{
type: FETCH_SERVICES,
payload: request
};
}
I assume that you think this.props.fetchServices()
will update the services
reducer and then will pass the services
as a prop via the mapStateToProps
.
If this is correct, note that you are fetching inside componentWillMount
and this is a BIG no no.
Quote from the componentWillMount
DOCS:
Avoid introducing any side-effects or subscriptions in this method.
You should fetch data in componentDidMount
.
In addition, you probably think the render method won't invoked until you got your data back from the ajax request. You see, react won't wait for your ajax call to get back with the data, the render
method will be invoked no matter what, so the first render
call will try to map
on an empty array of services
(you got an empty array as initial state in your reducer i assume).
Then your renderServices
function will get an empty array as data
and data.services
is indeed undefined
hence when you try to access data.services.name
you get the error:
"Cannot read property 'name' of undefined"
Just use a condition in your render:
<tbody>
{this.props.services && this.props.services.map(this.renderServices)}
</tbody>
Edit
As a followup to your comment, you are trying to map on an object
but .map
works on arrays. so actually you should map on services.services.map(...)
instead of services.map
, Though you still need to check if it's exists.
I've made a working example with your code, i did not include redux and ajax requests but i used the same data you are using and i'm passing it only on the second render of ServicesList
, so it basically has the same scenario you are facing.
I've even added a timeout to mimic a delay + added a loading indicator to demonstrate what you can (or should) do with conditional rendering.
const fakeData = {
services: [
{
name: "redemption-service",
versions: {
desired: "20170922_145033",
deployed: "20170922_145033"
},
version: "20170922_145033"
},
{
name: "client-service",
versions: {
desired: "20170608_094954",
deployed: "20170608_094954"
},
version: "20170608_094954"
},
{
name: "content-rules-service",
versions: {
desired: "20170922_130454",
deployed: "20170922_130454"
},
version: "20170922_130454"
}
]
};
class ServicesList extends React.Component {
componentDidMount() {
this.props.fetchServices();
}
render() {
const { services } = this.props;
return (
<table className="table table-hover">
<thead>
<tr>
<th>Service Name</th>
</tr>
</thead>
<tbody>
{services.services ? (
services.services.map(this.renderServices)
) : (
this.renderLoader()
)}
</tbody>
</table>
);
}
renderLoader() {
return (
<tr>
<td>Loading...</td>
</tr>
);
}
renderServices(data) {
const name = data.name;
return (
<tr key={name}>
<td>{name}</td>
</tr>
);
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: {}
};
this.fetchServices = this.fetchServices.bind(this);
}
fetchServices() {
setTimeout(() => {
this.setState({ data: { ...fakeData } });
}, 1500);
}
render() {
const { data } = this.state;
return (
<div>
<ServicesList services={data} fetchServices={this.fetchServices} />
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>