90

I want to render my component after my ajax request is done.

Below you can see my code

var CategoriesSetup = React.createClass({

    render: function(){
        var rows = [];
        $.get('http://foobar.io/api/v1/listings/categories/').done(function (data) {
            $.each(data, function(index, element){
                rows.push(<OptionRow obj={element} />);
            });
           return (<Input type='select'>{rows}</Input>)

        })

    }
});

But i get the error below because i am returning render inside the done method of my ajax request.

Uncaught Error: Invariant Violation: CategoriesSetup.render(): A valid ReactComponent must be returned. You may have returned undefined, an array or some other invalid object.

Is there a way to wait for my ajax request to end before start rendering?

tuna
  • 6,211
  • 11
  • 43
  • 63
  • 3
    Also, a small nitpick but having a data retrieval in a render() routine is not really a great idea. Keep render() to rendering and abstract out the rest. Also, you may only want to get that data once, not every time the component is rendered. – Phil Cooper Sep 09 '15 at 16:52

3 Answers3

138

There are two ways to handle this, and which you choose depends on which component should own the data and the loading state.

  1. Move the Ajax request into the parent and conditionally render the component:

    var Parent = React.createClass({
      getInitialState: function() {
        return { data: null };
      },
    
      componentDidMount: function() {
        $.get('http://foobar.io/api/v1/listings/categories/').done(function(data) {
          this.setState({data: data});
        }.bind(this));
      },
    
      render: function() {
        if (this.state.data) {
          return <CategoriesSetup data={this.state.data} />;
        }
    
        return <div>Loading...</div>;
      }
    });
    
  2. Keep the Ajax request in the component and render something else conditionally while it's loading:

    var CategoriesSetup = React.createClass({
      getInitialState: function() {
        return { data: null };
      },
    
      componentDidMount: function() {
        $.get('http://foobar.io/api/v1/listings/categories/').done(function(data) {
          this.setState({data: data});
        }.bind(this));
      },
    
      render: function() {
        if (this.state.data) {
          return <Input type="select">{this.state.data.map(this.renderRow)}</Input>;
        }
    
        return <div>Loading...</div>;
      },
    
      renderRow: function(row) {
        return <OptionRow obj={row} />;
      }
    });
    
dcodesmith
  • 9,590
  • 4
  • 36
  • 40
Michelle Tilley
  • 157,729
  • 40
  • 374
  • 311
  • 6
    I have stumbled across this answer in 2017, do these the two best solutions still remain the best to use? – Dan Mar 30 '17 at 10:22
  • @Dan React renders UI based on props and state, so the core concept will remain the same — do the Ajax request, set some state, and re-render something. However, patterns like higher order components have become more popular, demonstrating how one might abstract away the complexities. – Michelle Tilley Apr 02 '17 at 06:47
  • 1
    `if (this.state.data)` should be `if (this.state && this.state.data)` because sometimes state can be null. – Timo Ernst Apr 21 '17 at 09:12
  • @Timo Hm, in what case would `this.state` be `null`? – Michelle Tilley Apr 24 '17 at 20:06
  • @MichelleTilley I don't know why but for me this.state is null initially in `render()` – Timo Ernst Apr 27 '17 at 18:27
  • I made this little helper component. It takes out some of the pain of showing loading state, making async requests user events and stuff like that. I've used it a lot in my latest project. http://www.anamuser.com/2017/03/26/react-asynccontainer/ – AnAmuser May 22 '17 at 07:52
  • 6
    @Timo initialize the state in the constructor – Louis Aug 21 '17 at 01:18
  • @MichelleTilley when I try to set state inside the async call (I'm using the fetch API), it works but it constantly re-renders, terrible for performance. I came here trying to find the correct way to do this, any advice? – Big Guy Mar 04 '18 at 21:10
  • @BigGuy Hm, not sure what you mean. If you can create a small JSBin or similar to demonstrate the problem it'd be helpful! – Michelle Tilley Mar 05 '18 at 03:44
  • Just posting an update to this answer that, if possible, it's also recommended to cancel the promises on the `componentWillUnmount` lifecycle method. If you need cancelable promises there are suggestions i.e. [this](https://gist.github.com/bvaughn/982ab689a41097237f6e9860db7ca8d6). Also worth noting that React has made updates to make async rendering more intuitive see [blog post](https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html#fetching-external-data) – aug Apr 02 '18 at 08:03
  • great. please mention the version of the library. – sg28 Feb 06 '19 at 20:11
7

The basic example of async rendering of components is below:

import React                from 'react';
import ReactDOM             from 'react-dom';        
import PropTypes            from 'prop-types';

export default class YourComponent extends React.PureComponent {
    constructor(props){
        super(props);
        this.state = {
            data: null
        }       
    }

    componentDidMount(){
        const data = {
                optPost: 'userToStat01',
                message: 'We make a research of fetch'
            };
        const endpoint = 'http://example.com/api/phpGetPost.php';       
        const setState = this.setState.bind(this);      
        fetch(endpoint, {
            method: 'POST',
            body: JSON.stringify(data)
        })
        .then((resp) => resp.json())
        .then(function(response) {
            setState({data: response.message});
        });
    }

    render(){
        return (<div>
            {this.state.data === null ? 
                <div>Loading</div>
            :
                <div>{this.state.data}</div>
            }
        </div>);
    }
}
Roman
  • 19,236
  • 15
  • 93
  • 97
0

Async state management (Playground)

The following solution allows for async state management and can be used for HTTP related requirements if implemented correctly.

Requirements

  • Only re-render elements consuming the observable.
  • Automatically subscribe and unsubscribe from the observable.
  • Support multiple and joint observables.
  • Provide a loading state
  • Simple and easy implementation

Expected behaviour

return (
    <Async select={[names$]}>
        {result => <div>{result}</div>}
    </Async>
);

The provided example above will subscribe to the observable names$. The content/children of the Async component will re-render when next is fired on the observable, not causing the current component to re-render.

Async Component

export type AsyncProps<T extends any[]> = { select: { [K in keyof T]: Observable<T[K]> }, children: (result?: any[]) => JSX.Element };
export type AsyncState = { result?: any[] };

export default class Async<T extends any[]> extends Component<AsyncProps<T>, AsyncState> {

    private subscription!: Subscription;

    constructor(props: AsyncProps<T>) {
        super(props);
        this.state = {};
    }

    componentDidMount() {
        this.subscription = combineLatest(this.props.select)
            .subscribe(result => this.setState({ result: result as T }))
    }

    componentWillUnmount() {
        this.subscription.unsubscribe();
    }

    render() {
        return (
            <Fragment>
                {this.props.children(this.state.result)}
            </Fragment>
        );
    }

}