I have a component that returns an object called Progress
, inside of which is an array called Results
. This array has objects with various properties, one of which is called total
{
Progress: {
count: 100,
results: [
{total: 4, ...},
{total: 10, ...},
...
]
}
}
The component, Dashboard
, gets the data from state and maps it the Progress property.
export class Dashboard extends Component {
static propTypes = {
progress: PropTypes.object.isRequired,
getProgress: PropTypes.func.isRequired,
totalResults: PropTypes.number.isRequired
}
componentDidMount() {
this.props.getProgress()
}
...
}
const selectProgress = state => state.progressReducer.progress
const mapStateToProps = state => ({
progress: selectProgress(state),
})
export default connect(mapStateToProps, { getProgress })(Dashboard)
The issue I have now is how can I add a new property which is derived from progress?
I understand I need to use a Selector but I cannot see where/how to do this.
For example, I know I can do something trivial (and pointless) like this:
const mapStateToProps = state => ({
progress: selectProgress(state),
count: selectProgress(state).count
})
which adds another property count
to the component (yes it's just duplicated the property inside progress, hence why it is pointless).
What I need to do is something like this:
const mapStateToProps = state => ({
progress: selectProgress(state),
resultsTotal: <loop through the results array and sum the property total>
})
1 - What I have tried
I tried this even though I understand it isn't meant to be this way. This is to illustrate hopefully what I am trying to do - AFTER I've got progress, pass it to some function to calculate the total and return that as a property to the component:
const selectResults = progress => {
progress.results.reduce((acc, result) => {
acc + result.total
}, 0)
}
const mapStateToProps = state => ({
progress: selectProgress(state),
totalResults: selectResults(progress)
})
2 - What I have tried
I thought this would have worked, by basically letting the render view call function at the point needed in the JSX:
export class Dashboard extends Component {
static propTypes = {
progress: PropTypes.object.isRequired,
getProgress: PropTypes.func.isRequired,
}
componentDidMount() {
this.props.getProgress()
}
totalResults() {
if (this.props.progress.results)
return this.props.progress.results.reduce((acc, result) => {
acc + result.total
}, 0)
}
render() {
...
<SummaryCard title='Students' value={this.totalResults()} />
...
}
}
I am now wondering why this didn't work - I had to add this line:
if (this.props.progress.results)
because progress is of course empty when this function executes (ie I guess because it executes when the component first mounts, and the store has not returned the data yet).
One solution that I found to this problem is to use the excellent reselect library. From their github page:
So I had to create a second selector and chain it to the first one.
Below you can see that progressSelector
will pass its result (the progress
data) onto the next selector in the chain (totalResultsSelector
):
Here is the full component:
import React, { Component } from 'react';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createSelector } from 'reselect'
import { getProgress } from '../../actions/progress';
import SummaryCard from '../SummaryCard';
import { People } from 'react-bootstrap-icons';
import { Book } from 'react-bootstrap-icons';
import { Award } from 'react-bootstrap-icons';
import styles from './styles.scss';
export class Dashboard extends Component {
static propTypes = {
progress: PropTypes.object.isRequired,
getProgress: PropTypes.func.isRequired,
totalResults: PropTypes.number.isRequired
}
componentDidMount() {
this.props.getProgress()
}
render() {
return (
<div className={styles.wrapper}>
<Row xs={1} sm={3}>
<Col>
<SummaryCard title='Students' value={this.props.totalResults} icon={<People />} />
</Col>
<Col>
<SummaryCard title='Courses' value={this.props.progress.count} icon={<Book />} />
</Col>
<Col>
<SummaryCard title='Certified' value='0' icon={<Award />} />
</Col>
</Row>
</div>
)
}
}
const progressSelector = state => state.progressReducer.progress
const totalResultsSelector = createSelector (
progressSelector,
progress => {
if (!progress.results) return 0
const total = progress.results.reduce((acc, result) => {
return acc + result.total
}, 0)
return total
}
)
const mapStateToProps = state => ({
progress: progressSelector(state),
totalResults: totalResultsSelector(state)
})
export default connect(mapStateToProps, { getProgress })(Dashboard)