Batman
Batman

Reputation: 6363

How do I render a component when the data is ready?

I'm trying to figure out how to populate/render a component when the data is ready? Essentially I have a script that queries my server which returns data, then I parse it and make it into an collection with the properties I need. Then in another file, I have the react component that's looking for that object but they're running at the same time so the object doesn't exist when the component is looking for it.

I'm not sure how to proceed.

This is my component:

let SliderTabs = React.createClass({
    getInitialState: function() {
        return { items: [] }
    },
    render: function() {
        let listItems = this.props.items.map(function(item) {
            return (
                <li key={item.title}>
                    <a href="#panel1">{item.title}</a>
                </li>
            );
        });

    return (
            <div className="something">
                <h3>Some content</h3>
                    <ul>
                        {listItems}
                    </ul>
            </div>
        );
    }
});

ReactDOM.render(<SliderTabs items={home.data.slider} />,                
    document.getElementById('slider-tabs'));

How I'm getting my data:

var home = home || {};

home = {
  data: {
    slider: [],
    nav: []
  },
  get: function() {

    var getListPromises = [];

    $.each(home.lists, function(index, list) {
      getListPromises[index] = $().SPServices.SPGetListItemsJson({
        listName: home.lists[index].name,
        CAMLViewFields: home.lists[index].view,
        mappingOverrides: home.lists[index].mapping
      })
      getListPromises[index].list = home.lists[index].name;
    })

    $.when.apply($, getListPromises).done(function() {
      home.notice('Retrieved items')
      home.process(getListPromises);
    })
  },
  process: function(promiseArr) {
    var dfd = jQuery.Deferred();

    $.map(promiseArr, function(promise) {
      promise.then(function() {
        var data = this.data;
        var list = promise.list;

        // IF navigation ELSE slider
        if (list != home.lists[0].name) {
          $.map(data, function(item) {
            home.data.nav.push({
              title: item.title,
              section: item.section,
              tab: item.tab,
              url: item.url.split(",")[0],
              path: item.path.split("#")[1].split("_")[0]
            })
          })
        } else {
          $.map(data, function(item) {
            home.data.slider.push({
              title: item.title,
              url: item.url.split(",")[0],
              path: item.path.split("#")[1]
            })
          })
        }
      })
    })

    console.log(JSON.stringify(home.data))
    dfd.resolve();
    return dfd.promise();
  }
}

$(function() {
  home.get()
})

Upvotes: 3

Views: 12493

Answers (6)

Batman
Batman

Reputation: 6363

I got a few answers but I was still having a lot of trouble understanding how to accomplish what I was asking for. I understand that I should be retrieving the data with the components but I currently don't know enough about React to do that. I obviously need to spend more time learning it but for now I went with this:

Essentially, I added the property ready to my home object:

home.ready: false,
home.init: function() {
        // check if lists exist
        home.check()
        .then(home.get)
        .then(function() {
          home.ready = true; 
        })
    }

Then I used componentDidMount and a setTimeout to check when the data is ready and set the results to the this.state

let SliderTabs = React.createClass({
    getInitialState: function() {
        return {items:[]}
    },
    componentDidMount: function() {
        let that = this;

        function checkFlag() {
           if(home.ready == false) {
              window.setTimeout(checkFlag, 100); /* this checks the flag every 100 milliseconds*/
           } else {
              that.setState({items: home.data.slider})
           }
        }
        checkFlag();
    },
    render: function() {
        let listItems = this.state.items.map(function(item) {
            return (
                <li key={item.title}>
                    <a href="#panel1">{item.title}</a>
                </li>
                );
        });

        return (
            <div className="something">
                <h3>Some content</h3>
                <ul>
                    {listItems}
                </ul>
            </div>
            );
    }
});

ReactDOM.render(<SliderTabs/>,        
    document.getElementById('slider-tabs'));

Probably not the React way but it seems to work.

Upvotes: 0

Emrys Myrooin
Emrys Myrooin

Reputation: 2231

You should test the length of the data collection. If the collection is empty, return a placeholder (a loading wheel for exemple). In other cases, you can display the data collection as usual.

const SliderTabs = ({items}) => {
    let listItems = <p>Loading data...</p>

    if(items.length != 0) 
        listItems = items.map(item => 
            <li key={item.title}>
                <a href="#panel1">{item.title}</a>
            </li>
        )

    return (
        <div className="something">
            <h3>Some content</h3>
                <ul>
                    {listItems}
                </ul>
        </div>
    )
}

ReactDOM.render(
    <SliderTabs items={home.data.slider} />,                
    document.getElementById('slider-tabs')
)

I have use the functional way to define a React Component as it's the recommended way while you don't need of a state, refs or lifecycle methodes.

If you want to use this in a ES6 classe or with React.createCompnent (shoud be avoid), just use the function as the render function. (don't forget to extract items form the props)


EDIT : By reading the new answers, I've realised that I haven't fully answered.

If you want the view to be updated when the data are loaded, You have to integrate a little more your data fetching code. A basic pattern in React is to separate your components in tow type : the Containers Component and the Presentational Component.

The Containers will only take care of the logic and to fetch the useful data. In the other hand, the Presentational Components will only display the data given by the Container.

Here a little example : (try it on jsfidle)

Test utilities

var items = [{title: "cats"},{title: "dogs"}]

//Test function faking a REST call by returning a Promise.
const fakeRest = () => new Promise((resolve, reject) =>
  setTimeout(() => resolve(items), 2000)
)

Container Component

//The Container Component that will only fetch the data you want and then pass it to the more generic Presentational Component
class SliderTabList extends React.Component {
  constructor(props) { //
    super(props)
    //You should always give an initial state (if you use one of course)
    this.state = { items : [] }
  }

  componentDidMount() {
    fakeRest().then(
      items => this.setState({ items }) //Update the state. This will make react rerender the UI.
    )
  }

  render() {
    //Better to handle the fact that the data is fetching here.
    //This let the Presentational Component more generic and reusable
    const {items} = this.state
    return (
      items.length == 0 
        ? <p>Loading Data...</p>
        : <List items={items} />
    )
  }
}

Presentational Component

//The Presenational Component. It's called List because, in fact, you can reuse this component with other Container Component. Here is power of React.
const List = ({items}) => {
    //Prepare the rendering of all items
    const listItems = items.map(item => 
      <li key={item.title}>
        <a href="#panel1">{item.title}</a>
      </li>
    )

    //Simply render the list.
    return (
      <div className="something">
        <h3>Some content</h3>
          <ul>
            {listItems}
          </ul>
      </div>
    )
}

Rendering the App

//Mount the Container Component. It doesn't need any props while he is handling his state itself
ReactDOM.render(
    <SliderTabList />,                
    document.getElementById('slider-tabs')
)

Rather than checking for the length to not being 0, you also can initialise items to null in the state, to be able to differentiate fetching data from empty data. An other common way os to put a flag (a boolean in fetchingData int the state) to know if a data is fetching or not. But, in lots of articles, it's generaly recomended to have a state as litle as possible and then calculate all you need from it. So here, I sugest you to check for the length or the null.

Upvotes: 0

tobiasandersen
tobiasandersen

Reputation: 8680

A common way to do this in React is to keep track of when data is being fetched. This can be done e.g. by having a isFetching field in your state:

// This would be your default state
this.state = {
  isFetching: false
};

Then, when you fire off the request (preferably in componentDidMount) you set isFetching to true using:

this.setState({ isFetching: true });

And finally, when the data arrives, you set it to false again:

this.setState({ isFetching: false });

Now, in your render function you can do something like this:

render () {
 return (
    <div className="something">
      <h3>Some content</h3>
      {this.state.isFetching ? <LoadingComponent /> : (
         <ul>
           {listItems}
         </ul>
      )}
    </div> 
  )
}

By using state, you don't have to worry about telling your component to do something, instead it reacts to changes in the state and renders it accordingly.

Update:

If you plan on actually using React, I'd suggest you change your approach into what I've described above (read more in the React docs). That is, move the code you have in your get function into your React component's componentDidMount function. If that's not possible, you can probably just wait to call

ReactDOM.render(
  <SliderTabs items={home.data.slider} />,                
  document.getElementById('slider-tabs')
);

until your data has arrived.

Upvotes: 3

Jkarttunen
Jkarttunen

Reputation: 7621

Put that data loading in parent component that updates the props of your component as the data is being loaded.

Use default props instead of default state, since you are not using state at all in your example. Replace the 'getInitialState' with this:

   getDefaultProps: function() {
     return {
       items: []
     };
   }

Upvotes: 0

Igorsvee
Igorsvee

Reputation: 4201

Here is the explaination of React's way of doing these type of things, tl;dr - render the component immediately and either display loading indicator until the data is ready or return null from the render method.

Upvotes: 0

Mike Robinson
Mike Robinson

Reputation: 8955

Ordinarily, you would arrange a response to an OnLoad event, which will "fire" once the data has been loaded.

I agree with Emrys that your code should also be prepared to test whether the data is available yet, and to "do something reasonable" on the initial draw (when it probably isn't). But no, you would not then "poll" to see if the data has arrived:   instead, you arrange to be notified when the event fires. At that time, you would (for example) re-draw the UI components to reflect the information.

I kindly refer you to the React documentation to see exactly how this notification is done ...

Upvotes: -2

Related Questions