Chad D Chad D - 3 months ago 19
React JSX Question

ReactJS: I have a click handler that is skipping the first props method

import React from 'react';
import ReactDOM from 'react-dom';
import axios from 'axios';

var mode=[ 'recent', 'alltime'];

class Header extends React.Component {
constructor(){
super()
}

render(){
return <h2>Free Code Camp Leader board</h2>
}
}


class Leader extends React.Component {
constructor(props){
super(props)
this.state = {
users: [],
val: props.m
}
}

componentWillReceiveProps(props){
this.setState({val: props.m})
this.ajax();
}

componentWillMount() {
this.ajax();
}

ajax(){
this.serverRequest =
axios.get("https://fcctop100.herokuapp.com/api/fccusers/top/"+this.state.val)
.then((result) => {
console.log(result.data);
var leaders = result.data;
this.setState({
users: leaders
});
});
}

componentWillUnmount() {
this.serverRequest.abort();
}

render() {
console.log(this.state, this.props)
return (
<div className='container'>

<div className="tbl">
<table className="table">
<thead>
<tr>
<th>Name</th>
<th>Recent </th>
<th>Alltime</th>
</tr>
</thead>
<tbody>
{this.state.users.map(function(data, index){
return (<tr key={index}><td><img src={data.img} className="img img-thumbnail" width="50"/>{data.username}</td>
<td id='recent'>{data.recent}</td>
<td id='alltime'>{data.alltime}</td></tr>)
})}
</tbody>
</table>
</div>
</div>
)
}
}


class App extends React.Component{
constructor() {
super(),
this.state={val: mode[0]},
this.onClick= this.onClick.bind(this)
}

onClick() {
this.setState({val: this.state.val === mode[0]? mode[1] : mode[0]})
}

render() {
return (
<div>
<div className='header'>
<Header />
<button onClick={this.onClick} >{this.state.val==mode[0]? mode[1] : mode[0]}</button>
</div>
<div>
<Leader m={this.state.val} />
</div>
</div>
);
}
}

export default App;


For some reason the app loads properly, but on the first click of the button inside of is clicked, it does nothing; however, it works just fine on subsequent clicks, aside from being out of sync with the text on the button. Is there something wrong with the use of componentWillReceiveProps method?

JCD JCD
Answer

The problem is how you're doing your AJAX and managing React state. Let's take a look:

  componentWillReceiveProps(props){
    this.setState({val: props.m})
    this.ajax();
  }
  ajax(){
    this.serverRequest = 
          axios.get("https://fcctop100.herokuapp.com/api/fccusers/top/"+this.state.val)
           .then((result) => {
             console.log(result.data);
             var leaders = result.data;
             this.setState({
               users: leaders
           });
      });
  }

Let's walk through what happens here:

  1. The component receives a new prop value. componentWillReceiveProps is called.
  2. Inside componentwillReceiveProps the first thing that happens is you call setState. Which is fine... except that state won't update until after componentWillReceiveProps completely finishes running. From the React docs:

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.

So for the moment this does nothing - state remains exactly as it is for the time being; it has an outdated value! I'm not sure if it's explicitly documented but as far as I know, the only reliable place to access state after a this.setState is in the render() function.

  1. this.ajax() is called. Inside ajax() is an axios.get() call. Notice here how you're formatting a URL with this.state.val in it. At this point the component state has still not updated, so you're sending a GET request to the wrong URL.
  2. ajax() finishes running, and so does componentWillReceiveProps(). Sometime after this point is when statewill actually update. But, you've already initiated an AJAX call with the wrong URL, so it's too late now!

In a nutshell: this.setState does not update state right away and that's your problem. You're always seeing a result from AJAX that's "one state" too old.

How to fix it

It's easy actually. Instead of ajax relying on whatever this.state.val is at the moment it is called, rewrite it to accept it as a parameter.

  componentWillReceiveProps(props){
    this.setState({val: props.m})
    this.ajax(props.m);
  }
  // You have to modify componentWillMount() too because it should pass a parameter
  // No need to access state though - just use the prop directly
  componentWillMount() {
      this.ajax(this.props.val); 
  }
  // ajax() now accept a parameter it will use to construct its URL
  ajax(value){
    this.serverRequest = 
          axios.get("https://fcctop100.herokuapp.com/api/fccusers/top/"+value)
           .then((result) => {
             console.log(result.data);
             var leaders = result.data;
             this.setState({
               users: leaders
           });
      });
  }

Other notes

It's very weird of you to be taking in changing props and then for some reason transferring them to state. You can just use props directly, and whenever they change, use componentWillReceiveProps() do do your AJAX calls or whatever.

Comments