Taras Yaremkiv Taras Yaremkiv - 6 days ago 4
React JSX Question

React click handler function is working after 2 clicks

I'm working on the "Camper's Leaderboard" challenge from FreeCodeCamp. So there's such thing that I have to implement: "I can toggle between sorting the list by how many brownie points they've earned in the past 30 days and by how many brownie points they've earned total."

I added onclick handler to the list's header cells, but it changes a view after two clicks on the element instead of one. In a few words:

_handlerAll() and _handler30() are executed after two clicks in a row, not one.
Here is the code:



"use strict";
class TableBox extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
sortAllTime: true
};
}

loadCampersFromServer() {
let templateURL = 'https://fcctop100.herokuapp.com/api/fccusers/top/';
let url = (this.state.sortAllTime) ? (templateURL + 'alltime') : (templateURL + 'recent');
fetch(url)
.then(
(response) => {
if (response.status !== 200) {
console.log('Looks like there was a problem. Status Code: ${response.status}');
return;
}
response.json().then((data) => {
this.setState({data: data});

})
}
)
.catch(function (err) {
console.log('Fetch Error :-S', err);
});
}

_handlerAll() {
this.setState({sortAllTime: true});
this.loadCampersFromServer();
console.log('all');
}

_handler30() {
this.setState({sortAllTime: false});
this.loadCampersFromServer();
console.log('notAll');
}

componentDidMount() {
this.loadCampersFromServer();
}

render() {
return <CampersList _data={this.state.data}
_handlerAll={this._handlerAll.bind(this)} _handler30={this._handler30.bind(this)}/>;
}
}

class CampersList extends React.Component {
constructor(props) {
super(props);
}

render() {
let campersNodes = this.props._data.map((element, index) => {
return (
<Camper user={element} index={index}/>

);
});

return (
<table>
<tr>
<th>#</th>
<th>Camper's Name</th>
<th onClick={this.props._handler30}>Points in past 30 days</th>
<th onClick={this.props._handlerAll}>All time points</th>
</tr>
{campersNodes}
</table>
)
}
}

class Camper extends React.Component {
constructor(props) {
super(props);
}

render() {
return (
<tr>
<td>{this.props.index + 1}</td>
<td>
<img src={this.props.user.img} alt="logo"/>
<span>{this.props.user.username}</span>
</td>
<td>{this.props.user.recent}</td>
<td>{this.props.user.alltime}</td>
</tr>
)
}
}

ReactDOM.render(<TableBox />, document.getElementById('root'));

div#root {
margin: 0 auto;
width: 50%; }

img {
width: 32px; }

span {
padding: 20px 5px; }

table {
font-family: 'Arial';
margin: 25px auto;
border-collapse: collapse;
border: 1px solid #eee;
border-bottom: 2px solid #ccc0b8;
box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.1), 0px 10px 20px rgba(0, 0, 0, 0.05), 0px 20px 20px rgba(0, 0, 0, 0.05), 0px 30px 20px rgba(0, 0, 0, 0.05); }
table tr:hover {
background: #f4f4f4; }
table tr:hover td {
color: #555; }
table tr th, table tr td {
color: #999;
border: 1px solid #eee;
padding: 12px 35px;
border-collapse: collapse; }
table tr th {
background: #619d9f;
color: #fff;
text-transform: uppercase;
font-size: 12px; }
table tr th.last {
border-right: none; }

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




Answer

This part of the docs is relevant:

setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value.

That's exactly what you are doing: You are calling this.setState({sortAllTime: true}); and immediately access this.state.sortAllTime in loadCampersFromServer. But that returns the old value. So even though eventually the state is updated, the data you load is the "old" data. Only the next (second) click loads the data you expected.


Instead, pass the value for sortAllTime to loadCampersFromServer and set the state value for sortAllTime together with data:

loadCampersFromServer(sortAllTime) {
  // perform Ajax request to get `data` for `sortAllTime`
  // ...
  this.setState({sortAllTime, data});
  // ...
}

"use strict";
class TableBox extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            data: [],
            sortAllTime: true
        };
    }

    loadCampersFromServer(sortAllTime) {
        let templateURL = 'https://fcctop100.herokuapp.com/api/fccusers/top/';
        let url = sortAllTime ? (templateURL + 'alltime') : (templateURL + 'recent');
        fetch(url)
            .then(
                (response) => {
                    if (response.status !== 200) {
                        console.log('Looks like there was a problem. Status Code: ${response.status}');
                        return;
                    }
                    response.json().then((data) => {
                        this.setState({data, sortAllTime});

                    })
                }
            )
            .catch(function (err) {
                console.log('Fetch Error :-S', err);
            });
    }

    _handlerAll() {
        this.loadCampersFromServer(true);
    }

    _handler30() {
        this.loadCampersFromServer(false);
    }

    componentDidMount() {
        this.loadCampersFromServer();
    }

    render() {
        return <CampersList _data={this.state.data}
        _handlerAll={this._handlerAll.bind(this)} _handler30={this._handler30.bind(this)}/>;
    }
}

class CampersList extends React.Component {
    constructor(props) {
        super(props);
    }

    render() {
        let campersNodes = this.props._data.map((element, index) => {
            return (
                <Camper user={element} index={index}/>

            );
        });

        return (
            <table>
                <tr>
                    <th>#</th>
                    <th>Camper's Name</th>
                    <th onClick={this.props._handler30}>Points in past 30 days</th>
                    <th onClick={this.props._handlerAll}>All time points</th>
                </tr>
                {campersNodes}
            </table>
        )
    }
}

class Camper extends React.Component {
    constructor(props) {
        super(props);
    }

    render() {
        return (
            <tr>
                <td>{this.props.index + 1}</td>
                <td>
                    <img src={this.props.user.img} alt="logo"/>
                    <span>{this.props.user.username}</span>
                </td>
                <td>{this.props.user.recent}</td>
                <td>{this.props.user.alltime}</td>
            </tr>
        )
    }
}

ReactDOM.render(<TableBox />, document.getElementById('root'));
div#root {
  margin: 0 auto;
  width: 50%; }

img {
  width: 32px; }

span {
  padding: 20px 5px; }

table {
  font-family: 'Arial';
  margin: 25px auto;
  border-collapse: collapse;
  border: 1px solid #eee;
  border-bottom: 2px solid #ccc0b8;
  box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.1), 0px 10px 20px rgba(0, 0, 0, 0.05), 0px 20px 20px rgba(0, 0, 0, 0.05), 0px 30px 20px rgba(0, 0, 0, 0.05); }
  table tr:hover {
    background: #f4f4f4; }
    table tr:hover td {
      color: #555; }
  table tr th, table tr td {
    color: #999;
    border: 1px solid #eee;
    padding: 12px 35px;
    border-collapse: collapse; }
  table tr th {
    background: #619d9f;
    color: #fff;
    text-transform: uppercase;
    font-size: 12px; }
    table tr th.last {
      border-right: none; }
<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>