54

I need to fetch some information before rendering my component. The information will be provided by an API and fetched with an AJAX call.

I'm just trying to wait 10 seconds before rendering my component but it says:

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

Can I render my component after the fulfillment of a promise?

/** Page Login */
class Login extends React.Component {

  /**
   * @constructor
   * @param {object} props La fonction super() appelle le parent pour y transmettre ses propriétés
   */
  constructor(props) {
    super(props);

    this.handleFormSubmit = this.handleFormSubmit.bind(this);
  }

  /**
   * Reçoit les valeurs des formulaires
   */
  handleFormSubmit(data) {
    const { dispatch } = this.props;

    dispatch(fetchLoginAuth(data));
  }

  normalRender() {
    return (
      <div id="login-page">
        <div className="container-fluid">
          <div className="row">
            <div className="col-md-2">
              <Link to="/" className="home-link"><img src={BASE_URL + '/assets/img/logo.svg'} alt="Logo" /></Link>
            </div>
          </div>
          <div className="row">
            <div className="col-lg-4 col-lg-offset-4">
              <h1><FormattedMessage {...messages.loginPageTitle} /></h1>
            </div>
          </div>
          {React.cloneElement(this.props.children || <div />, { onSubmit: this.handleFormSubmit, login: this.props.login })}
        </div>
      </div>
    );
  }

  /**
   * Render le component - ReactTransitionGroup
   * @return {JSX} Rend la page Registration
   */
  render() {
    setTimeout(this.normalRender, 10000);
  }
}

I use ES6 with JSX, redux, an universal router with react-router.

Thank you very much for your help!

scunliffe
  • 62,582
  • 25
  • 126
  • 161
Crak_mboutin
  • 1,046
  • 2
  • 12
  • 18
  • [react-block-ui](https://availity.github.io/react-block-ui/) could be used. It also has [support for redux actions](https://availity.github.io/react-block-ui/components/reduxblockui/) where actions can be used to determine to block or not. – TheSharpieOne Aug 09 '17 at 18:07

6 Answers6

62

Here's what I do normally:

class Login extends React.Component {
    constructor(props) {
        //IMPLEMENT OTHER JUNK HERE
        this.state = {
            data: null //This is what our data will eventually be loaded into
        };
    }
    componentDidMount() {
        this.loadData();
    }
    loadData() {
        /*LOAD DATA, INSERT BELOW LINE IN CALLBACK FUNCTION
            this.setState({
                data: //LOADED DATA
            });
        */
    }
    render() {
        if (!this.state.data) {
            return <div />
        }

        //WE HAVE DATA, DO A NORMAL RENDER
        return (
            <div id="login-page">
                <div className="container-fluid">
                    <div className="row">
                        <div className="col-md-2">
                            <Link to="/" className="home-link"><img src={BASE_URL + '/assets/img/logo.svg'} alt="Logo" /></Link>
                        </div>
                    </div>
                    <div className="row">
                        <div className="col-lg-4 col-lg-offset-4">
                            <h1><FormattedMessage {...messages.loginPageTitle} /></h1>
                        </div>
                    </div>
                    {React.cloneElement(this.props.children || <div />, { onSubmit: this.handleFormSubmit, login: this.props.login })}
                </div>
            </div>
        );
    }
}

Here's a breakdown of what is going to happen...

  1. Component is going to load
  2. componentDidMount() fires, runs loadData()
  3. loadData() starts ajax request, returns before ajax request returns data because we love asynchronous data loads
  4. render() runs. Since this.state.data is null, we have pass into the if block, and <div /> is returned.
  5. Ajax data load finishes, and a this.setState() call is made, which forces a re-render.
  6. render() runs again. Since this.state.data contains a value now, we skip over the if block and render our normal stuff.

Edit (11 Oct 2019): Migrated componentWillMount() to componentDidMount()

pzrq
  • 1,626
  • 1
  • 18
  • 24
molson504x
  • 1,180
  • 10
  • 18
  • 1
    for me, this only worked when i added the .length property to the check, even when state was being set with an empty array. If you set state to null first then it works as stated. But I guess an empty array evaluates to true in react for some reason. – Jarg7 Jul 19 '18 at 21:40
12

Always let React render.

While you're doing something asynchronous, show a loading spinner or something.

render() {
  <div>
    { this.state.isLoading &&
    <div>Loading.. please wait!</div>
    }
    { !this.state.isLoading &&
    <div>My data has arrived!</div>
    }
  </div>
}
azium
  • 20,056
  • 7
  • 57
  • 79
  • I don't like loading spinner, i want some material design loading bar at the top or like youtube. Thank you for your answer! It's a good way – Crak_mboutin Jan 26 '16 at 21:05
  • What do I do if there already has been a DOM rendered server side ? I do not want to replace it with a loader. I want to keep it. But when the page loads client side, it kill my server side rendered content :( – fabien Aug 27 '17 at 21:28
6

Update: This answer is out of date now. You are better off moving away from classes and using hooks.

#######################

An alternative way to the accepted answer using the constructor. Personally I find this a little cleaner.

class Menu extends Component {

state = {}

constructor(props) {
    super(props)
    loadData().then(data =>
        this.setState({data: data})
    )
}

async loadData() {
  //get your data
}

render() {
    if (isEmpty(this.state)) {
        return <div>Loading</div>
    }
    return (
        <div id="site">
            {data}
        </div>
    )
 }
3

You can try something like

/** Page Login */
class Login extends React.Component {
    constructor(props) {
    ...
      this.state = {
        ready: false
      };
    }
    componentWillMount() {
       setTimeout(this.handleLoading, 10000);
    }
    handleLoading() {
      this.setState({ ready: true });
    }
    render() {
       if(!this.state.ready)
         return null;
       return normalRender();
    }
}
Franco Risso
  • 1,572
  • 2
  • 13
  • 18
  • 3
    Never prevent render. Returning null may throw an error. You're always better off rendering an empty div. – molson504x Jan 26 '16 at 20:27
  • 5
    That's wrong: check [here](https://facebook.github.io/react/docs/component-specs.html#render): You can also return null or false to indicate that you don't want anything rendered. Behind the scenes, React renders a – Franco Risso Jan 26 '16 at 22:28
  • 1
    Franco Risso: makes sense. From what I've read up on, you're better off returning an empty div, but I forget what the logic was behind that. – molson504x Jan 27 '16 at 13:09
  • 2
    @molson504x the problem with empty divs is that you can't know if divs have some styles that shows something in the screen by default, with – Franco Risso Jan 27 '16 at 14:10
0

Suspending the render seems hacky...

Why not render a part of your component with some placeholder-sub-component.. and then, when the ajax call finishes, fire an action to change the state and render your original component.

It'll be better both in terms of UX and elegance.

DougieHauser
  • 460
  • 6
  • 14
0

if you have to handle same case in multiple places or you don't have to use state just because to track async function callback in presentational components, there is an easy way to take out these problems please refer react-promise.

react-promise provide a hook called usePromise, sandbox (you can play around)

import React from "react";
import usePromise from "react-promise";
import "./styles.css";

const prom = new Promise((resolutionFunc, rejectionFunc) => {
  // call your async return values after succesfull response
  setTimeout(function () {
    resolutionFunc(<h1> Hello World </h1>);
  }, 5000);
});
const App = () => {
  const { value, loading } = usePromise(prom);
  if (loading) return <div> loading </div>;
  return <div>{value}</div>;
};
export default App;

you can see Hello world after some delay

Sunil Kumar
  • 420
  • 4
  • 13