How to create API calls in REACT and FLUX
Asked Answered
R

6

16

I'm new to react and flux and I am having a hard time trying to figure out how to load data from a server. I am able to load the same data from a local file with no issues.

So first up I have this controller view (controller-view.js) that passes down initial state to a view (view.js)

controller-view.js

var viewBill = React.createClass({
getInitialState: function(){
    return {
        bill: BillStore.getAllBill()
    };
},
render: function(){
    return (
        <div>
            <SubscriptionDetails subscription={this.state.bill.statement} />
        </div>
    );
}
 });
 module.exports = viewBill;

view.js

var subscriptionsList = React.createClass({
propTypes: {
    subscription: React.PropTypes.array.isRequired
},
render: function(){

   return (
        <div >
            <h1>Statement</h1>
            From: {this.props.subscription.period.from} - To {this.props.subscription.period.to} <br />
            Due: {this.props.subscription.due}<br />
            Issued:{this.props.subscription.generated}
        </div>
    );
}
 });
 module.exports = subscriptionsList;

I have an actions file that loads the INITAL data for my app. So this is data that is not called by as user action, but called from getInitialState in the controller view

InitialActions.js

var InitialiseActions = {
initApp: function(){
    Dispatcher.dispatch({
        actionType: ActionTypes.INITIALISE,
        initialData: {
            bill: BillApi.getBillLocal() // I switch to getBillServer for date from server
        }
    });
}
};
module.exports = InitialiseActions;

And then my data API looks like this

api.js

var BillApi = {
getBillLocal: function() {
    return billed;
},
getBillServer: function() {
    return $.getJSON('https://theurl.com/stuff.json').then(function(data) {

        return data;
    });
}
};
module.exports = BillApi;

And this is the store store.js

var _bill = [];
var BillStore = assign({}, EventEmitter.prototype, {
addChangeListener: function(callback) {
    this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback) {
    this.removeListener(CHANGE_EVENT, callback);
},
emitChange: function() {
    this.emit(CHANGE_EVENT);
},
getAllBill: function() {
    return _bill;
}
});

Dispatcher.register(function(action){
switch(action.actionType){
    case ActionTypes.INITIALISE:
        _bill = action.initialData.bill;
        BillStore.emitChange();
        break;
    default:
        // do nothing
}
});

module.exports = BillStore;

So as I mentioned earlier, when I load data locally using BillApi.getBillLocal() in actions everything works fine. But when I change to BillApi.getBillServer() I get the followind errors in the console...

Warning: Failed propType: Required prop `subscription` was not specified in     `subscriptionsList`. Check the render method of `viewBill`.
Uncaught TypeError: Cannot read property 'period' of undefined

I also added a console.log(data) to BillApi.getBillServer() and I can see that the data is returned from the server. But it is displayed AFTER I get the warnings in the console which I believe may be the issue. Can anyone offer some advice or help me to fix it? Sorry for such a long post.

UPDATE

I made some changes to the api.js file (check here for change and DOM errors plnkr.co/edit/HoXszori3HUAwUOHzPLG ) as it was suggested that the issue is due to how I handle the promise. But it still seems to be the same issue as you can see in the DOM errors.

Relativity answered 6/10, 2015 at 22:11 Comment(3)
What are you passing down tho subscriptionsList? It is looking for this.props.subscriptions and it is non-existent so you get Cannot read property 'period' of undefined. My guess is you also have some type of race condition as well. Flux is asynchronous by nature...Lumbricalis
I thought maybe thats why I was getting the 'cannot read' error - because of the race condition. The data maybe had not loaded yet? Any tips how to fix that?Relativity
Yeah, you can use the callback method like ultralame suggested or you can give the _bill a default object such as var _bill = { subscriptions: [] } so when you do getInitialState you just get the bill via store.getAllBill(). then when the component mounts, the data is fetched, and the store will emit the change and update your stateLumbricalis
S
8

This is an async issue. Using $.getJSON().then() is not enough. Since it returns a promise object, you have to handle the promise at invocation by doing something like api.getBill().then(function(data) { /*do stuff with data*/ });

I made a CodePen example with the following code:

function searchSpotify(query) {
  return $.getJSON('http://ws.spotify.com/search/1/track.json?q=' + query)
  .then(function(data) {
    return data.tracks;
  });  
}

searchSpotify('donald trump')
.then(function(tracks) {
  tracks.forEach(function(track) {
    console.log(track.name);
  });
});
Schreib answered 7/10, 2015 at 2:39 Comment(4)
Thanks for your answer. But if you check api.js where I make the call to the server with getBillServer you can see I am using a .then() and return data to the action. Is this not correct?Relativity
Updated my answer. Basically, the first .then() returns a promise, so you have to use another .then() to get your data.Schreib
Thats great thanks. I am just trying it now. Just to be clear, the searchSpotify function in this case would go into my api.js file and the function call (searchSpotify('donald trump')) into my actions file?Relativity
Afraid not. I created a plunkr so you can see how I added the code. I also added the dom errors so you can see that it is reaching the api. But it seems the component is loading before the data arrives. I must have something wrong elsewhere - plnkr.co/edit/HoXszori3HUAwUOHzPLGRelativity
C
3

It looks like from your code that the intended flow is something like:

  • some component fires initialize action,
  • initialize action calls API
  • which waits for results from server (I think here is where things break down: your component render starts before results from server are back),
  • then passes the result to the store,
  • which emits change and
  • triggers a re-render.

In a typical flux setup, I would advise to structure this somewhat different:

  • some component calls API (but does not fire action to dispatcher yet)
  • API does getJSON and waits for server results
  • only after results are received, API triggers the INITIALIZE action with received data
  • store responds to action, and updates itself with results
  • then emits change
  • which triggers re-render

I am not so familiar with jquery, promises and chaining, but I think this would roughly translate into the following changes in your code:

  • controller-view needs a change listener to the store: add a componentDidMount() function that adds an event listener to flux store changes.
  • in controller-view, the event listener triggers a setState() function, which fetches the most recent _bill from the store.
  • move the dispatcher.dispatch() from your actions.js to your api.js (replacing return data);

That way, your component initially should render some 'loading' message, and update as soon as data from server is in.

Contestation answered 12/10, 2015 at 20:55 Comment(0)
C
2

An alternative method would be to check if the prop of subscription exists before you play with the data.

Try modifying your code to look a bit like this:

render: function(){

  var subscriptionPeriod = '';
  var subscriptionDue = ['',''];
  var subscriptionGenerated = '';

  if(this.props.subscription !== undefined){
       subscriptionPeriod = this.props.subscription.period;
       subscriptionDue = [this.props.subscription.due.to,this.props.subscription.due.from];
       subscriptionGenerated = this.props.subscription.generated;
  }

  return (
    <div >
        <h1>Statement</h1>
        From: {subscriptionPeriod[0]} - To {subscriptionPeriod[1]} <br />
        Due: {subscriptionDue}<br />
        Issued:{subscriptionGenerated}
    </div>
);
}

In the render function before the return try adding the following: if(this.props.subscription != undefined){ // do something here }

Due your data changing the state of the top level component it will retrigger the render once it has the data with the subscription prop being defined.

Clink answered 7/10, 2015 at 6:23 Comment(2)
Thank you. Where would I place this code? I presume it shouldn't wrap the render function in controller view?Relativity
Many thanks for writing the code. I can see it is a better way. But the problem still remains where the initial data is not loading. I still get the error "Warning: Failed propType: Required prop subscription was not specified in subscriptionsList. Check the render method of viewBill." I guess the state is not changing when the data loads and the retrigger is not happening. I can see the data has loaded in the console window. Any ideas what might be causing it?Relativity
P
2

If I understand correctly you could try with something like this

// InitialActions.js


var InitialiseActions = {
initApp: function(){
    BillApi.getBill(function(result){
      // result from getJson is available here
      Dispatcher.dispatch({
          actionType: ActionTypes.INITIALISE,
          initialData: {
              bill: result
          }
      });
    });
}
};
module.exports = InitialiseActions;

//api.js

var BillApi = {
    getBillLocal: function() {
        console.log(biller);
        return biller;
    },
    getBill: function(callback) {
      $.getJSON('https://theurl.com/stuff.json', callback);
    }
};

$.getJSON does not return the value from the http request. It makes it available to the callback. The logic behind this is explained in detail here: How to return the response from an asynchronous call?

Platter answered 9/10, 2015 at 19:44 Comment(0)
E
2

I'll separate my Actions, Stores and Views (React components).

First of all, I'd implement my Action like this:

import keyMirror from 'keymirror';


import ApiService from '../../lib/api';
import Dispatcher from '../dispatcher/dispatcher';
import config from '../env/config';


export let ActionTypes = keyMirror({
  GetAllBillPending: null,
  GetAllBillSuccess: null,
  GetAllBillError: null
}, 'Bill:');

export default {
  fetchBills () {
    Dispatcher.dispatch(ActionTypes.GetAllBillPending);

    YOUR_API_CALL
      .then(response => {
        //fetchs your API/service call to fetch all Bills
        Dispatcher.dispatch(ActionTypes.GetAllBillSuccess, response);
      })
      .catch(err => {
        //catches error if you want to
        Dispatcher.dispatch(ActionTypes.GetAllBillError, err);
      });
  }
};

The next is my Store, so I can keep track of all changes that suddenly may occur during my api call:

class BillStore extends YourCustomStore {
  constructor() {
    super();

    this.bindActions(
      ActionTypes.GetAllBillPending, this.onGetAllBillPending,
      ActionTypes.GetAllBillSuccess, this.onGetAllBillSuccess,
      ActionTypes.GetAllBillError  , this.onGetAllBillError
    );
  }

  getInitialState () {
    return {
      bills : []
      status: Status.Pending
    };
  }

  onGetAllBillPending () {
    this.setState({
      bills : []
      status: Status.Pending
    });
  }

  onGetAllBillSuccess (payload) {
    this.setState({
      bills : payload
      status: Status.Ok
    });
  }

  onGetAllBillError (error) {
    this.setState({
      bills : [],
      status: Status.Errors
    });
  }
}


export default new BillStore();

Finally, your component:

import React from 'react';

import BillStore from '../stores/bill';
import BillActions from '../actions/bill';


export default React.createClass({

  statics: {
    storeListeners: {
      'onBillStoreChange': BillStore
     },
  },

  getInitialState () {
    return BillStore.getInitialState();
  },

  onBillStoreChange () {
    const state = BillStore.getState();

    this.setState({
      bills  : state.bills,
      pending: state.status === Status.Pending
    });
  },

  componentDidMount () {
    BillActions.fetchBills();
  },

  render () {
    if (this.state.pending) {
      return (
        <div>
          {/* your loader, or pending structure */}
        </div>
      );
    }

    return (
      <div>
        {/* your Bills */}
      </div>
    );
  }

});
Echopraxia answered 14/10, 2015 at 18:44 Comment(0)
B
0

Assuming you are actually getting the data from your API, but are getting it too late and errors are thrown first, try this: In your controller-view.js, add the following:

componentWillMount: function () {
    BillStore.addChangeListener(this._handleChangedBills);
},

componentWillUnmount: function () {
    BillStore.removeChangeListener(this._handleChangedBills);
},

_handleChangedBills = () => {
    this.setState({bill: BillStore.getAllBill()});
}

And in your getInitialState function, give an empty object with the structure that your code expects (specifically, have a 'statement' object inside it). Something like this:

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

What is happening is that when you are getting your initial state, it isn't fetching from the store properly, and so will return an undefined object. When you then ask for this.state.bill.statement, bill is initialized but undefined, and so it cannot find anything called statement, hence why you need to add it in. After the component has had a bit more time (this is an async problem like the other posters said), it should fetch properly from the store. This is why we wait for the store to emit the change for us, and then we grab the data from the store.

Balsam answered 15/10, 2015 at 22:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.