Search code examples
javascriptreactjsreact-reduxlodash

Method renders correctly when triggered one at a time, but not using _.map in React-Redux container


I have React-Redux container where a bunch of buttons are dynamically generated. Below them are Select All and Clear Selection buttons. When a button is clicked, it is sent to a method where it setState that the button is active and sends information about the selected button to an action that determines how things are rendered below the buttons.

Anyway, when I select a button one at a time, it adds to the state and permits the selecting of multiple buttons. When I use Select All, it only selects the last button. In both cases, the value of the button is being passed to the method as a string one at a time (at least I am pretty sure that is what _.map is essentially doing), so not sure why this isn't working.

class BasicQueryCuts extends Component {
    constructor(props) {
        super(props);
        this.state = {
            cuts: {}
        }

        this.selectAll = this.selectAll.bind(this);
    }

    // This is where state is being set and data sent to the Redux store
    // It affects how some other components are rendered
    onCutSelect(cut) {
        let cuts = {...this.state.cuts, [cut]: cut}
        this.setState({
            cuts
        }, () => this.props.basicQueryResults(this.state.cuts));
    }

    // The select all where it should basically take what is the value
    // of rendered buttons and pass it to the onCutSelect method
    // However, this will only actually select the last button.
    selectAll() {
        const { data } = this.props;
        _.map(
            data, c => {
                this.onCutSelect(c.Cut);
            }
        )        
    }

    // These buttons will allow selecting everything, or clearing the selection
    renderAllNothingButtons() {
        const { data } = this.props;

        // generate the list of cuts
        if (data) {
            return (
                <Row>
                    <Col>
                        <Button color='primary' className='cuts-btn' onClick={this.selectAll}>
                            Select All
                        </Button>
                    </Col>
                    <Col>
                        <Button color='danger' className='cuts-btn' onClick={this.clearSelection}>
                            Clear All
                        </Button>
                    </Col>
                </Row>
            )            
        }
    }

    // Render the individual buttons
    // Selecting one at a time does work as expected: it adds to state
    renderCutsButtons() {
        const { data } = this.props;

        return (
            <Row>
                {_.map(
                    data, c => {
                        return (
                            <Col>
                                <Button 
                                    className={this.state.cuts.hasOwnProperty(c.Cut) ? 'cuts-btn-active' : 'cuts-btn'}
                                    key={c.Cut}
                                    value={c.Cut}
                                    onClick={event => this.onCutSelect(event.target.value)}
                                >
                                    {c.Cut}
                                </Button>
                            </Col>
                        )   
                    }
                )}
            </Row>
        )
    }

    render() {
        // In the case of selecting one button at a time it will print:
        // {Cut1: "Cut1", Cut2: "Cut2", etc.}
        // In the case of using Select All, it will just print the last:
        // {CutLast: "CutLast"}
        console.log(this.state.cuts);
        return (
            <div className='results-bq-cuts'>
                {this.renderCutsButtons()}
                {this.renderAllNothingButtons()}
            </div>
        )
    }
}

Solution

  • There might be some other problems with the code, but one problem that can explain this behavior is that the updater-object parameter cuts to the setState call refers to this.state.

    A setState call in a React event handler does not immediately update the state, but rather queues the update. At some point after your event handler finishes, these batched updates are processed to compute the new state. This means that when you call onCutSelect multiple times, you queue a series of updates, which each replace cuts by a clone of the original cuts with only one cut updated. When these updates are processed, the last update wins, and only the last button is selected.

    The solution is to use an updater function instead of an object:

    this.setState((prevState, props) => ({/* state update */})) (see the setState docs).

    The updater function will get the up-to-date state resulting from applying previous updates, so you can incrementally update each cut. In your code you can do something like this:

    onCutSelect(cut) {
        this.setState(
          ({cuts: prevCuts}) => ({cuts: {...prevCuts, [cut]: cut}}),
          () => this.props.basicQueryResults(this.state.cuts)
        );
    }
    

    Also, if this.props.data is an array, you can simply use this.props.data.map((c) => ..). No need to use lodash or underscore.