NightMarcher
NightMarcher

Reputation: 159

Passing Ajax/service data between components in React.js

I am trying to pass data received in one component of a React application. On success I am taking the received data and setting the state and then trying to pass that data as a property of the next component. Once inside the second component I need to access the passed data via this.state so I can change the state in that component later. I seem to be encountering an issue with the DOM rendering before the data is received from the service. I have tried passing an already loaded array of values in place of this.state.data in <List data={this.state.data}/> and it seems to execute fine. How can assure that I have received the data from the service before rendering the DOM so that the data is passed all the way down to each component.

EDIT: added full implementation of the List element so explain the use of this.state

This is basically what I am trying to do:

var Box = React.createClass({

  getInitialState: function() {
    return {data: []};
  },

  loadTodosFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      type: 'GET',
      cache: false,
      success: function(dataResponse) {
        this.setState({data: dataResponse});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },

  componentDidMount: function() {
    this.loadFromServer();
  },

  render: function() {
    return (<List data={this.state.data}/>);
  }
});


var List = React.createClass({

  getInitialState: function() {
    return {data: this.props.data};
  },

dragStart: function(e) {
  this.dragged = e.currentTarget;
  e.dataTransfer.effectAllowed = 'move';
  // Firefox requires dataTransfer data to be set
  e.dataTransfer.setData("text/html", e.currentTarget);
},

dragEnd: function(e) {
  this.dragged.style.display = "block";
  this.dragged.parentNode.removeChild(placeholder);
  // Update data
  var data = this.state.data;
  var from = Number(this.dragged.dataset.id);
  var to = Number(this.over.dataset.id);
  if(from < to) to--;
  if(this.nodePlacement == "after") to++;
  data.splice(to, 0, data.splice(from, 1)[0]);
  this.setState({data: data});
},

dragOver: function(e) {
  e.preventDefault();
  this.dragged.style.display = "none";
  if(e.target.className == "placeholder") return;
  this.over = e.target;
  // Inside the dragOver method
  var relY = e.clientY - this.over.offsetTop;
  var height = this.over.offsetHeight / 2;
  var parent = e.target.parentNode;

  if(relY > height) {
    this.nodePlacement = "after";
    parent.insertBefore(placeholder, e.target.nextElementSibling);
  }
  else if(relY < height) {
    this.nodePlacement = "before"
    parent.insertBefore(placeholder, e.target);
  }
},

  render: function() {
    var results = this.state.data;
      return (
        <ul>
            {
              results.map(function(result, i) {
                return (
                  <li key={i}>{result}</li>
                )
              })
            }
        </ul>
      );
    }
  });


ReactDOM.render(
  <Box url="/api/comments"/>, document.getElementById('content')
);

Upvotes: 2

Views: 1588

Answers (1)

KumarM
KumarM

Reputation: 1699

The reason why your data load subsequent to component load is not rendering the data is because of this line in your List.render function:

var results = this.state.data;

Essentially, you have made a copy of your original props and assigned them to the state in the List component using the getInitialState method. And after that your state and props are delinked. Which means that if the props.data changes on the List component, the state doesn't know about it, so therefore nothing gets re-rendered.

So, instead of using state to initialize the results variable, use props.

var results = this.props.data

Here's how it would look like:

var List = React.createClass({

  render: function() {
    var results = this.props.data;
      return (
        <ul>
            {
              results.map(function(result, i) {
                return (
                  <li key={i}>{result}</li>
                )
              })
            }
        </ul>
      );
    }
  });

Now anytime the data changes, props get updated and eventually the results get re-rendered.

Updated to address the comments from the OP:

If you want to update the state of the list but want to be notified every time the props at the parent change, then you want to use the method componentWillReceiveProps so that when the data is obtained the child List is notified. And in this method you can set the new state:

componentWillReceiveProps: function(newProps) {
    this.setState({data: this.props.data});
} 

Once you do this, react will re-render the list for you.

Another update: To illustrate how this works I have put together an example here.

And here's the JS code for this:

let todos = ["Run","Swim","Skate"];

class MyList extends React.Component{
  componentWillMount() {
    console.log("Props are: ", this.props);
    this.setState({list: this.props.items});
  }

  componentWillReceiveProps(newProps) {
    console.log("Received Props are: ", newProps);
    this.setState({list: newProps.items});
  }

  render() {
    return (<ul>
            {this.state.list.map((todo) => <li>{todo}</li>)}
            </ul>);
  }
}

class App extends React.Component{
  constructor() {
      super();
      console.log("State is: ", this.state);
  }

  componentWillMount() {
    this.setState({items: ["Fly"]});
  }

  componentDidMount() {
    setTimeout(function(){
      console.log("After 2 secs");
      this.setState({items: todos});
    }.bind(this), 2000);
  }

  render() {
      return (<MyList items={this.state.items}/>);
  }
}


ReactDOM.render(<App/>, document.getElementById("app"));

Upvotes: 2

Related Questions