How to resolve FOUC in React.js

2019-08-18 07:05发布

问题:

I have built react.js site from create-react-app. But in production mode, there is FOUC because styles are loaded after html is rendered.

Is there any way to resolve this? I have been searching google for answers, but haven't found proper one yet.

回答1:

FOUC

FOUC - so called Flash of Unstyled Content can be as very problematic as so many tries of solving this issue.

To the point

Let's consider following configuration of routing (react-router):

...
<PageLayout>
  <Switch>
    <Route exact path='/' component={Home} />
    <Route exact path='/example' component={Example} />
  <Switch>
</PageLayout>
...

where PageLayout is a simple hoc, containing div wrapper with page-layout class and returning it's children.

Now, let's focus on the component rendering based on route. Usually you would use as component prop a React Compoment. But in our case we need to get it dynamically, to apply feature which helps us to avoid FOUC. So our code will look like this:

import asyncRoute from './asyncRoute'

const Home = asyncRoute(() => import('./Home'))
const Example = asyncRoute(() => import('./Example'))

...

<PageLayout>
  <Switch>
    <Route exact path='/' component={Home} />
    <Route exact path='/example' component={Example} />
  <Switch>
</PageLayout>

...

to clarify let's also show how asyncRoute.js module looks like:

import React, { Component } from 'react'
import PropTypes from 'prop-types'

import Loader from 'components/Loader'

class AsyncImport extends Component {
  static propTypes = {
    load: PropTypes.func.isRequired,
    children: PropTypes.node.isRequired
  }

  state = {
    component: null
  }

  toggleFoucClass () {
    const root = document.getElementById('react-app')
    if (root.hasClass('fouc')) {
      root.removeClass('fouc')
    } else {
      root.addClass('fouc')
    }
  }

  componentWillMount () {
    this.toggleFoucClass()
  }

  componentDidMount () {
    this.props.load()
      .then((component) => {
        setTimeout(() => this.toggleFoucClass(), 0)
        this.setState(() => ({
          component: component.default
        }))
      })
  }

  render () {
    return this.props.children(this.state.component)
  }
}

const asyncRoute = (importFunc) =>
  (props) => (
    <AsyncImport load={importFunc}>
      {(Component) => {
        return Component === null
          ? <Loader loading />
          : <Component {...props} />
      }}
    </AsyncImport>
  )

export default asyncRoute

hasClass, addClass, removeClass are polyfills which operates on DOM class attribute.

Loader is a custom component which shows spinner.

Why setTimeout?

Just because we need to remove fouc class in the second tick. Otherwise it would happen in the same as rendering the Component. So it won't work.

As you can see in the AsyncImport component we modify react root container by adding fouc class. So HTML for clarity:

<html lang="en">
<head></head>
<body>
  <div id="react-app"></div>
</body>
</html>

and another piece of puzzle:

#react-app.fouc
    .page-layout *
        visibility: hidden

sass to apply when importing of specific component (ie.: Home, Example) takes place.

Why not display: none?

Because we want to have all components which rely on parent width, height or any other css rule to be properly rendered.

How it works?

The main assumption was to hide all elements until compoment gets ready to show us rendered content. First it fires asyncRoute function which shows us Loader until Component mounts and renders. In the meantime in AsyncImport we switch visibility of content by using a class fouc on react root DOM element. When everything loads, it's time to show everything up, so we remove that class.

Hope that helps!

Thanks to

This article, which idea of dynamic import has been taken (I think) from react-loadable.

Source

https://turkus.github.io/2018/06/06/fouc-react/