❮ zur Übersicht


Implementing a waiting component with user experience in mind

Giving fast feedback to users has been improved by single page applications over the request response cycle. However, there is one serious downside with this approach. Elements are popping out of the wild on various sections everytime. Particular data loading indicated by a waiting animation is affected with this phenomenon. In this blog I’d like to present you our solution of a UI component that takes care about delaying the rendering of the animation.

Disclaimer: we’re using React in our frontend (without server side rendering). In case you don’t know React: React provides lifecycle hooks for UI components like

  • render
  • willUpdate
  • or didUpdate.

These hooks can be used to do internal stuff your component requires to be rendered correctly. React components can either be updated with changing properties or updating state. Properties are actually the public API of the component. The state, however, is the antagonist which can only be updated by the component itself. Changing properties or state triggers specific lifecycle hooks and finally a rerendering of the component. Don’t hesitate to read the react docs for more detail.

tl;dr source code is available on github.

loading or not loading

At first we have to satisfy the basic need. The user must get feedback whether we’re loading data currently or not. The most simple component takes a boolean property that reflects the current state.

class Waiting extends React.Component {
  render() {
    return this.props.loading ? <div>loading...</div> : null;
  }
}

This component can now be used in our App. The loading info is visible as long as the loading flag is set to true and hidden as soon as the flag is toggled. MyDataView is just another component that takes care about rendering the data.

class MyApp extends React.Component {
  // initialState
  // no data existent and we're loading currently
  state = {
    data: null,
    loading: true
  };

  renderData() {
    return this.state.data ? <MyDataView data={this.state.data} /> : null;
  }

  render() {
    return (
      <div>
        <Waiting loading={this.state.loading} />
        {this.renderData()}
      </div>
    );
  }
}

One benefit of this solution is that we now have a reusable component. We don’t have to care about the visualisation stuff anymore at every place. It could render the div element with a static text or it could render some more advanced css animation. For instance we could change the loading animation to use this awesome codepen with refactoring the Waiting component implementation only. Consumers of the Waiting component wouldn’t have to be touched.

A second benefit is the really simple implementation of the Waiting component. Even without knowing React or JavaScript in detail you quickly see that a div or nothing is rendered.

pretend not loading when it’s fast

The next step is user experience improvement. We don’t want to render the loading text when the loading flag is toggled back to false within 100ms.

0.1 second is about the limit for having the user feel that the system is reacting instantaneously, meaning that no special feedback is necessary except to display the result. (Response times by Jakob Nielsen)

To keep changes small let us first map the loading property value to the internal component state. React takes care about calling render when either new properties are given or state is changed with setState. So in the constructor we’re mapping the original loading flag to render the initially intended state. Let’s say the yep, we’re currently loading state. Soon afterwards the property will eventually swap to nop, we’re finished loading. This can be intercepted by the componentWillReceiveProps lifecycle hook . Just like in the constructor we’re mapping the property to the internal state.

class Waiting extends React.Component {
+  constructor(props) {
+    super();
+    this.state = { loading: props.loading };
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (nextProps.loading !== this.props.loading) {
+      this.setState({ loading: nextProps.loading });
+    }
+  }
+
  render() {
-    return this.props.loading ? <div>loading...</div> : null;
+    return this.state.loading ? <div>loading...</div> : null;
  }
}

So far we’ve gained nothing but complexity /o\

Now to the interesting part. As soon as the Waiting component receives new properties we’re starting a timeout to update the internal state with a delay of 100ms. Remember react calls render on property changes as well as on state changes. So render is called two times now actually. The first time it renders the same as previously nop, we’re not loading. After 100ms setState is called which triggers the second render cycle yep, we’re loading.

class Waiting extends React.Component {
  constructor() { ... }

  componentWillReceiveProps(nextProps) {
    if (nextProps.loading !== this.props.loading) {
+     window.clearTimeout(this._loadingTimeout);
+     this._loadingTimeout = window.setTimeout(() => {
        this.setState({ loading: nextProps.loading });
+     }, 100);
   }

   render() { ... }
 }
}

But wait, what’s happening now when the loading property is swapped the other way around from yep to nop? Remember the implementation of MyApp from above?

class MyApp extends React.Component {
  // ...
  render() {
    return (
      <div>
        <Waiting loading={this.state.loading} />
        {this.renderData()}
      </div>
    );
  }
}

The Waiting component receives the updated loading flag false and delays it’s internal rendering while this.renderData() renders the actual data. So the loading info is shortly visible amongst the data. Fortunately this can be fixed easily. We just have to update immediately when the loading property is set to false.

class Waiting extends React.Component {
  constructor() { ... }

  componentWillReceiveProps(nextProps) {
    if (nextProps.loading !== this.props.loading) {
      window.clearTimeout(this._loadingTimeout);
+     if (nextProps.loading) {
        this._loadingTimeout = window.setTimeout(() => {
          this.setState({ loading: nextProps.loading });
        }, 100);
+     } else {
+       this.setState({ loading: false });
+     }
    }
  }

  render() { ... }
}

Now we’ve gained a good user experience by not displaying the loading info if the loading property is toggled from yay back to nop within 100ms. There is no flickering anymore \o/ However, we’ve payed with some complexity in the Waiting component and even have async stuff happening there. So testing consumers of the Waiting component could be confusing. But in my opinion the better user experience is worth the complexity and tests should be fine as long as shallowRendering is used. Otherwise we have to use the timemachine feature of the testing library (e.g. jest provides jest.useFakeTimers() and jest.runTimersToTime(100))

improved handling of data rendering

Currently we have a waiting component that takes care about delaying the loading info. But the consumer is still responsible to check itself whether the data is available and should be rendered or not.

renderData() {
  return this.state.data
    ? <MyDataView data={this.state.data} />
    : null;
}

However, my collegues and my humble self could live with this redundancy actually. It is explicit and the waiting component wouldn’t be bloated with more features and complexity. But in our project we had the following issue (amongst some others…)

Given MyDataView renders a list of items with a headline and other eye candy stuff. It takes care about rendering a no data info banner when the given list is empty. The default this.state.data value is an empty array instead of undefined or null to avoid the notorious Cannot read property XXX of undefined. Then the code snippet above results in always rendering MyDataView and therefore the no data info banner (empty array is a truthy expression).

The unwanted no data info banner could be avoided by adding the this.state.loading flag to the condition. But that’s not really satisfying since this adds more complexity which even will be copied and pasted into other components.

renderData() {
  return (this.state.data && !this.state.loading)
    ? <MyDataView data={this.state.data} />
    : null;
}

Furthermore… remember the actual challenge we tried to solve with the Waiting component which delays the rendering of the loading info? Exactly, we wanted to avoid flickering and displaying the loading info when the data is received within 100ms. Now we’ve added this again for MyDataView. The component will be unmounted and mounted within 42ms for instance. The new data is visible but all eye candy around the data list (like the headline) is gone and rerendered within one blink of an eye.

So let’s improve the Waiting component to handle the rendering of it’s children. We have two react techniques to implement this:

  • render props
  • function as child

Both are the same actually. The render prop pattern uses a function passed as component property to render something. The function as child pattern is… well… the same. children is just an additional property of a React component. The difference between render props and function as child is the syntax. Personally I prefer render props since this is more explicit and doesn’t leave room of misconception for people not knowing React and JSX in detail.

class RenderProps extends React.Component {
  render() {
    return <Waiting render={() => this.renderData()} />;
  }
}

class FunctionAsChild extends React.Component {
  render() {
    return <Waiting>{() => this.renderData()}</Waiting>;
  }
}

The first step is to extend the Waiting component with a render property. Instead of returning null when data is not loading we have to call this.props.render.

class Waiting extends React.Component {
  constructor() { ... }

  componentWillReceiveProps(nextProps) { ... }

+  renderContent() {
+    return this.state.loading ? <div>loading...</div> : this.props.render();
+  }
+
  render() {
-    return this.state.loading ? <div>loading...</div> : null;
+    return this.renderContent();
  }
}

And the consumer component has to be touched, too. The rendering is triggered by the Waiting component now.

class MyApp extends React.Component {
  // ...

  render() {
    return (
      <div>
-        <Waiting loading={this.state.loading} />
-        {this.renderData()}
+        <Waiting loading={this.state.loading} render={() => this.renderData()} />
      </div>
    );
  }
}

Now the Waiting component takes care about rendering the loading info and the desired content with a delay of 100ms.

Unfortunately we’re not finished yet… There is this gap of 100ms in which the stale data is still visible currently. Assuming that the App won’t be unmounted due to a runtime error cannot read property 'map' of null for instance. Let’s recap the lifecycle:

  • App.js updates with this.setState({ loading: true, data: null })
  • App.js render is invoked due to state change
  • Waiting.js componentWillReceiveProps is invoked which delays toggling the loading flag
  • Waiting.js render is invoked due to property change
  • App.js renderData is called -> BAAM

We could implement the shouldComponentUpdate hook verifying if this.state.loading has been toggled. So render is only called when the flag toggles. This would protect us from the exception in the use case above. Unfortunately, this would add another bug /o\. Complex beast it is… Implementing this hook would prevent updating the content when new data arrives without toggling the loading flag (e.g. cached data).

class Waiting extends React.Component {
  constructor() { ... }

  componentWillReceiveProps(nextProps) { ... }

  // this hook is no option actually
  shouldComponentUpdate(prevProps, prevState) {
    return (
      // checking loading flag prevents
      // rendering new stuff that arrives without toggling the flag
      prevState.loading !== this.state.loading
      // this is always true when defined as inline function,
      // otherwise an obsolete check
      && prevProps.renderData !== this.props.renderData
    );
  }

  renderContent() { ... }

  render() { ... }
}

So our solution is quite simple actually. After taking a (longer) coffee (beer) break we started reflection. And one brain had the awesome idea of three valid render outputs of the Waiting component.

  • the actual data (this.props.renderData)
  • the loading info
  • nothing (when we don’t know whether we’re loading or not)
class Waiting extends React.Component {
  constructor(props) {
    super();
    this.state = {
      loading: props.loading,
+     inDecision: false,
    };
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.loading !== this.props.loading) {
      window.clearTimeout(this._loadingTimeout);
      if (nextProps.loading) {
+       this.setState({ inDecision: true });
        this._loadingTimeout = window.setTimeout(() => {
          this.setState({
            loading: nextProps.loading,
+           inDecision: false,
          });
        }, 100);
      } else {
        this.setState({
          loading: false,
+         inDecision: false,
        });
      }
    }
  }

  renderContent() {
+   if (this.state.inDecision) {
+     return null;
+   }
    return this.state.loading ? <div>loading...</div> : this.props.render();
  }

  render() { ... }
}

Finally we have our Waiting component \o\/

If there wasn’t this feeling…

One thing we had to learn during our project was to take care about the whole lifecycle of a react component. A component is created with the constructor, then it is rendered into the DOM and is updated via property changes until it’s unmounted. Therefore we have to ensure the correct mapping of the properties to the internal state in the constructor, too. Components are not updated via componentWillReceiveProps when we have cached data for instance.

To fix this we have to set inDecision initially to true if the property loading is truthy. Additionally we have to implement the componentDidMount hook to start the timer which toggles the inDecision flag and renders the loading info after 100ms. Which is if componenWillReceiveProps didn’t clear the timeout already, of course.

class Waiting extends React.Component {
  constructor(props) {
    super();
    this.state = {
      loading: props.loading,
-     inDecision: false,
+     inDecision: props.loading,
    };
  }

+  componentDidMount() {
+    if (this.state.inDecision) {
+      this._loadingTimeout = window.setTimeout(() => {
+        this.setState({
+          loading: true,
+          inDecision: false
+        });
+      }, 100);
+    }
+  }
+
  componentWillReceiveProps(nextProps) { ... }

  renderContent() { ... }

  render() { ... }
}

That’s it for today. Thanks for reading :-)

The code can be found on github. Feel free to do whatever you want with it.

Tags: ux user experience react reactjs javascript