React-router: How to render loading state after immediately clicking a link, but before data has been fetched

Created on 25 Jun 2014  Â·  11Comments  Â·  Source: ReactTraining/react-router

I want to give my users instant response to <Link> activation, then have the route handler component dispatch an event to have its store loaded according to the params.

I'm also in the habit of injecting dependencies in props. Sneaking in the dependencies as enclosed variables is easy when every component is in the same file, but feels wrong the moment you put a component in its own file:

var Component = require('../component.jsx')(stores, dispatcher.fire);
React.renderComponent(Route({ handler: Component }), document.body);

I'd prefer this:

var Component = require('../component.jsx');

React.renderComponent(
    Route({ handler: Component,
            stores: stores,
            fire: dispatcher.fire  }
    ), document.body);

Component gets stores and fire as RNR-style "static props"; it can start watching the stores in componentDidMount, and fire events from its event handlers. All we need is fire that first event to load the data. So, in willTransitionTo, we calls fire on the dispatcher…

… except, we can't. There's no such argument, and we can't provide component like willTransitionFrom gets because the component hasn't been created yet. Indeed, we _want_ to handle this before the component is created so we can avoid a flash of not-even-trying-to-load unanticipated state. That leaves the route's static props as a way to provide willTransitionFrom access to the dependencies.

Am I missing some cleaner way to accomplish this?

Most helpful comment

Hi, all:

I create a mixin to display loading UI like this:

var NProgress = require('nprogress');

var ProgressMixin = {
  componentWillMount: function() {
    NProgress.start();
  },

  componentDidMount: function() {
    NProgress.done();
  }
};

Then add the mixin to every page:

var Page1 = React.createClass({
  mixins: [ProgressMixin],

  render: function() {
    return (
        <div className="Image">
          <h1>Page 1</h1>
        </div>
    );
  }
});

It works! When change the link, the loading UI appear.

But I have to add the mixin to every handler, is there any API for adding mixin to all route handlers?

Thanks very much.

All 11 comments

Hack-around: the components used as a route handler being kinda special, anyway, I'm using them as minimal life support around the domain-specific components. The Route creates the NounPage, which creates the Noun.

NounPage can use its getInitialState to peek at the store it's passing to Noun and, if necessary, use the fire method it's also passing to Noun to ask the store to start loading the content.

My dispatcher is synchronous, so the store's state will show that it's loading the data by the time Page hits its own getInitialState and asks the store for its state.

if I understand you correctly, you want a route handler to display some sort of loading UI while you fetch data?

var SomethingHandler = React.createClass({
  getInitialState: function() {
    return { loading: true, something: {} };
  },

  componentDidMount: function() {
    someStore.fetch(this.props.params.id, function(something) {
      this.setState({ loading: false, something: something });
    }.bind(this));
  },

  render: function() {
    if (this.state.loading) {
      return <div>loading ...</div>;
    }
    else {
      return <Something something={this.state.something}/>
    }
  }
});

Am I missing your question?

_Edit_: s/componentWillMount/componentDidMount/

Also, you can pass props into routes and they'll get sent down to your handlers:

<Route handler={App} store={store}/>

// ...
var App = React.createClass({
  componentDidMount: function() {
    this.props.store.whatev();
  }
});

componentWillMount makes the most sense. Cheers!

Hi, all:

I create a mixin to display loading UI like this:

var NProgress = require('nprogress');

var ProgressMixin = {
  componentWillMount: function() {
    NProgress.start();
  },

  componentDidMount: function() {
    NProgress.done();
  }
};

Then add the mixin to every page:

var Page1 = React.createClass({
  mixins: [ProgressMixin],

  render: function() {
    return (
        <div className="Image">
          <h1>Page 1</h1>
        </div>
    );
  }
});

It works! When change the link, the loading UI appear.

But I have to add the mixin to every handler, is there any API for adding mixin to all route handlers?

Thanks very much.

@minwe Hi, i am trying your solution but desnt work. I am writting in ES6, so its little different.

class Print extends React.Component {

  constructor(props)
  {
    super(props);
    this.state = {
      canvas: null,
      mixins: [ProgressMixin],
    }
    render(){
       return(
         <canvas className="canvas" ref="canvas" width="895" height="560"></canvas>
       )
    }
}

and mixin looks same. -

var NProgress = require('nprogress');

var ProgressMixin = {
    componentWillMount: function() {
        NProgress.start();
    },

    componentDidMount: function() {
        console.log('neco')
        NProgress.done();
    },

    start(){
        console.log('bla');
        NProgress.start();
    },
    stop(){
        NProgress.done();
    }
};

module.exports = ProgressMixin;

But still there is long time to render new page without any loading UI. I am rendering canvas and it takes a lot of time. I tried to get into start method console.log function, but its not invoken. So i think this method isnt called. Any tips ?

@uragecz

Mixins not work with ES6 Class, see https://facebook.github.io/react/docs/reusable-components.html#no-mixins .

You should use createClass.

@minwe createClass needs render method, so have i write just emty div ? If yes, it still doesnt work :/ i changed it to -

var ProgressMixin = React.createClass({
    componentWillMount: function() {
        NProgress.start();
    },

    componentDidMount: function() {
        NProgress.done();
    },

    start: function(){

        NProgress.start();
    },
    stop: function(){
        NProgress.done();
    },

    render: function(){
        return(<div></div>)
    }
});

module.exports = ProgressMixin;

Or u think to do remake my component which calls mixins: [ProgressMixin] ?

or you could use babel es6 decorators if you're working with webpack

create a file _decorators/nProgress.jsx_:

import NProgress from 'nprogress'

export default function nProgress(target) {
    const superComponentWillMount = target.prototype.componentWillMount
    const superComponentDidMount = target.prototype.componentDidMount
    target.prototype.componentWillMount = function() {
        if (typeof superComponentWillMount === 'function')
            superComponentWillMount.apply(this, arguments)
        NProgress.start()
    }
    target.prototype.componentDidMount = function() {
        if (typeof superComponentDidMount === 'function')
            superComponentDidMount.apply(this, arguments)
        NProgress.done()
    }
}

then simply annotate your components with "nProgress" like so:

import React, { PropTypes, Component } from 'react'
import { Link } from "react-router"
import { Jumbotron } from 'react-bootstrap'
import nProgress from '../decorators/nProgress'

@nProgress
export default class Reports extends Component {
...

@roby-rodriguez Please am sorry to ask this, when you mentioned using annotate I am really confused if I might have to set my project to static type(flow) or typescript as it is not working with normal js. I am using CRA and I dont want to eject is there a way I could modify the append to babelrc without ejecting to configure babel for the annotate

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Radivarig picture Radivarig  Â·  3Comments

misterwilliam picture misterwilliam  Â·  3Comments

jzimmek picture jzimmek  Â·  3Comments

ackvf picture ackvf  Â·  3Comments

hgezim picture hgezim  Â·  3Comments