React Redux - how to derive and then aggregate data in mapStateStateToProps using a selector?

667 Views Asked by At

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).

2

There are 2 best solutions below

0
On BEST ANSWER

One solution that I found to this problem is to use the excellent reselect library. From their github page:

  • Selectors can compute derived data, allowing Redux to store the minimal possible state.
  • Selectors are efficient. A selector is not recomputed unless one of its arguments changes.
  • Selectors are composable. They can be used as input to other selectors.

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)
6
On

mapStateToProps is a function. Currently you are using a short version to return an object immediately, but you can have it as a complex function and return an object in the end:

const mapStateToProps = state => {
    const progress = selectProgress(state);
    return {
        progress,
        totalResults: progress !== undefined ? selectResults(progress) : undefined
    }
}