React - Display loading screen while DOM is render

2019-01-20 21:36发布

This is an example from Google Adsense application page. The loading screen displayed before the main page showed after.

enter image description here

I don't know how to do the same thing with React because if I make loading screen rendered by React component, it doesn't display while page is loading because it has to wait for DOM rendered before.

Updated:

I made an example of my approach by putting screen loader in index.html and remove it in React componentDidMount() lifecycle method.

Example: https://nguyenbathanh.github.io

Source: https://github.com/nguyenbathanh/react-loading-screen

9条回答
仙女界的扛把子
2楼-- · 2019-01-20 22:13

Setting the timeout in componentDidMount works but in my application I received a memory leak warning. Try something like this.

constructor(props) {
    super(props)
    this.state = { 
      loading: true,
    }
  }
  componentDidMount() {
    this.timerHandle = setTimeout(() => this.setState({ loading: false }), 3500); 
  }

  componentWillUnmount(){
    if (this.timerHandle) {
      clearTimeout(this.timerHandle);
      this.timerHandle = 0;
    }
  }
查看更多
仙女界的扛把子
3楼-- · 2019-01-20 22:16

Since you render react into a DOM container - <div id="app"></div>, you can add a spinner to that container, and when react will load and render, the spinner will disappear.

The problem:

React will replace the contents of the container as soon as ReactDOM.render() is called. Even if you render null, the content would still be replaced by a comment - <!-- react-empty: 1 -->. This means that if you want to display the loader while react renders, a loader markup placed inside the container (<div id="app"><div class="loader"></div></div> for example) would not work.

Solution:

My solution is to add the loader class directly to the container, but with the :empty pseudo class. The spinner will be visible, as long as nothing is rendered into the container (comments don't count). As soon as react renders an element, the loader will disappear.


In the example you can see a component that renders null until it's ready. The container is the loader as well - <div id="app" class="app"></div>, and the loader's class will only work if it's :empty (see comments in code):

class App extends React.Component {
  state = {
    loading: true
  };

  componentDidMount() {
    // the setTimeout just simulates an async action, after which the component will render the content
    setTimeout(() => this.setState({ loading: false }), 1500);
  }
  
  render() {
    const { loading } = this.state;
    
    if(loading) { // if your component doesn't have to wait for an async action, remove this block 
      return null; // render null when app is not ready
    }
    
    return (
      <div>I'm the app</div>
    ); 
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('app')
);
.loader:empty {
  position: absolute;
  top: calc(50% - 4em);
  left: calc(50% - 4em);
  width: 6em;
  height: 6em;
  border: 1.1em solid rgba(0, 0, 0, 0.2);
  border-left: 1.1em solid #000000;
  border-radius: 50%;
  animation: load8 1.1s infinite linear;
}

@keyframes load8 {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.4.1/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.4.1/react-dom.js"></script>

<div id="app" class="loader"></div> <!-- add class loader to container -->

A variation on using the :empty pseudo class to/show hide selector, is setting the spinner as a sibling element to the app container, and showing it as long as the container is empty using the adjacent sibling combinator (+):

class App extends React.Component {
  state = {
    loading: true
  };

  componentDidMount() {
    setTimeout(() => this.setState({ loading: false }), 2500); // simulates loading of data
  }
  
  render() {
    const { loading } = this.state;
    
    if(loading) { // if your component doesn't have to wait for async data, remove this block 
      return null; // render null when app is not ready
    }
    
    return (
      <div>I'm the app</div>
    ); 
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('app')
);
#app:not(:empty) + .sk-cube-grid {
  display: none;
}

.sk-cube-grid {
  width: 40px;
  height: 40px;
  margin: 100px auto;
}

.sk-cube-grid .sk-cube {
  width: 33%;
  height: 33%;
  background-color: #333;
  float: left;
  animation: sk-cubeGridScaleDelay 1.3s infinite ease-in-out;
}

.sk-cube-grid .sk-cube1 {
  animation-delay: 0.2s;
}

.sk-cube-grid .sk-cube2 {
  animation-delay: 0.3s;
}

.sk-cube-grid .sk-cube3 {
  animation-delay: 0.4s;
}

.sk-cube-grid .sk-cube4 {
  animation-delay: 0.1s;
}

.sk-cube-grid .sk-cube5 {
  animation-delay: 0.2s;
}

.sk-cube-grid .sk-cube6 {
  animation-delay: 0.3s;
}

.sk-cube-grid .sk-cube7 {
  animation-delay: 0s;
}

.sk-cube-grid .sk-cube8 {
  animation-delay: 0.1s;
}

.sk-cube-grid .sk-cube9 {
  animation-delay: 0.2s;
}

@keyframes sk-cubeGridScaleDelay {
  0%,
  70%,
  100% {
    transform: scale3D(1, 1, 1);
  }
  35% {
    transform: scale3D(0, 0, 1);
  }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.4.1/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.4.1/react-dom.js"></script>

<div id="app"></div>
<!-- add class loader to container -->

<div class="sk-cube-grid">
  <div class="sk-cube sk-cube1"></div>
  <div class="sk-cube sk-cube2"></div>
  <div class="sk-cube sk-cube3"></div>
  <div class="sk-cube sk-cube4"></div>
  <div class="sk-cube sk-cube5"></div>
  <div class="sk-cube sk-cube6"></div>
  <div class="sk-cube sk-cube7"></div>
  <div class="sk-cube sk-cube8"></div>
  <div class="sk-cube sk-cube9"></div>
</div>

查看更多
Root(大扎)
4楼-- · 2019-01-20 22:25

The workaround for this is:

In your render function do something like this:

constructor() {
    this.state = { isLoading: true }
}

componentDidMount() {
    this.setState({isLoading: false})
}

render() {
    return(
        this.state.isLoading ? *showLoadingScreen* : *yourPage()*
    )
}

Initialize isLoading as true in the constructor and false on componentDidMount

查看更多
欢心
5楼-- · 2019-01-20 22:25

I'm also using React in my app. For requests I'm using axios interceptors, so great way to make loader screen (fullpage as you showed an example) is to add class or id to for example body inside interceptors (here code from official documentation with some custom code):

// Add a request interceptor
axios.interceptors.request.use(function (config) {
    // Do something before request is sent
     document.body.classList.add('custom-loader');
     return config;
  }, function (error) {
    // Do something with request error
    return Promise.reject(error);
  });

// Add a response interceptor
axios.interceptors.response.use(function (response) {
    // Do something with response data
       document.body.classList.remove('custom-loader');
       return response;
  }, function (error) {
    // Do something with response error
    return Promise.reject(error);
  }); 

And then just implement in CSS your loader with pseudo-elements (or add class or id to different element, not body as you like) - you can set color of background to opaque or transparent, etc... Example:

custom-loader:before {
    background: #000000;
    content: "";
    position: fixed;
    ...
}

custom-loader:after {
    background: #000000;
    content: "Loading content...";
    position: fixed;
    color: white;
    ...
}
查看更多
Viruses.
6楼-- · 2019-01-20 22:28

I had to deal with that problem recently and came up with a solution, which works just fine for me. However, I've tried @Ori Drori solution above and unfortunately it didn't work just right (had some delays + I don't like the usage of setTimeout function there).

This is what I came up with:

index.html file

Inside head tag - styles for the indicator:

<style media="screen" type="text/css">

.loading {
  -webkit-animation: sk-scaleout 1.0s infinite ease-in-out;
  animation: sk-scaleout 1.0s infinite ease-in-out;
  background-color: black;
  border-radius: 100%;
  height: 6em;
  width: 6em;
}

.container {
  align-items: center;
  background-color: white;
  display: flex;
  height: 100vh;
  justify-content: center;
  width: 100vw;
}

@keyframes sk-scaleout {
  0% {
    -webkit-transform: scale(0);
    transform: scale(0);
  }
  100% {
    -webkit-transform: scale(1.0);
    opacity: 0;
    transform: scale(1.0);
  }
}

</style>

Now the body tag:

<div id="spinner" class="container">
  <div class="loading"></div>
</div>

<div id="app"></div>

And then comes a very simple logic, inside app.js file (in the render function):

const spinner = document.getElementById('spinner');

if (spinner && !spinner.hasAttribute('hidden')) {
  spinner.setAttribute('hidden', 'true');
}

How does it work?

When the first component (in my app it's app.js aswell in most cases) mounts correctly, the spinner is being hidden with applying hidden attribute to it.

What's more important to add - !spinner.hasAttribute('hidden') condition prevents to add hidden attribute to the spinner with every component mount, so actually it will be added only one time, when whole app loads.

查看更多
Ridiculous、
7楼-- · 2019-01-20 22:31

If anyone looking for a drop-in, zero-config and zero-dependencies library for the above use-case, try pace.js (http://github.hubspot.com/pace/docs/welcome/).

It automatically hooks to events (ajax, readyState, history pushstate, js event loop etc) and show a customizable loader.

Worked well with our react/relay projects (handles navigation changes using react-router, relay requests) (Not affliated; had used pace.js for our projects and it worked great)

查看更多
登录 后发表回答