1

I am running into problems using React Loadable with route based code splitting using Webpack 3.11.

When I try to render my app on the server my async modules immediately resolve without waiting for the promise. Thus the SSR output becomes <div id="root"></div>.

App.js:

const App = () => (
  <Switch>
    {routes.map((route, index) => (
      <Route key={index} path={route.path} render={routeProps => {
        const RouteComponent = route.component
        return <RouteComponent {...routeProps} />
      }} />
    ))}
  </Switch>
)

I've defined my async route components with React Loadable like this:

Routes.js

function Loading ({ error }) {
  if (error) {
    return 'Oh nooess!'
  } else {
    return <h3>Loading...</h3>
  }
}

const Article = Loadable({
  loader: () => import(/* webpackChunkName: "Article" */ '../components/contentTypes/Article'),
  loading: Loading
})

const Page = Loadable({
  loader: () => import(/* webpackChunkName: "Page" */ '../components/contentTypes/Page'),
  loading: Loading,
  render (loaded, props) {
    let Component = WithSettings(loaded.default)
    return <Component {...props}/>
  }
})

export default [
  {
    path: `/:projectSlug/:env${getEnvironments()}/article/:articleSlug`,
    component: Article,
    exact: true
  },
  {
    path: `/:projectSlug/:env${getEnvironments()}/:menuSlug?/:pageSlug?`,
    component: Page
  }
]

WithSettings.js

export default (WrappedComponent: any) => {
  class WithSettings extends React.Component<WithSettingsProps, WithSettingsState> {
    static displayName = `WithSettings(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`

    state = {
      renderWidth: 1200
    }

    componentDidMount () {
      this.loadSettings({ match: { params: { projectSlug: '', env: '' } } })
      window.addEventListener('resize', this.onResize)
      this.onResize()
    }

    componentWillUnmount () {
      if (isClient) {
        window.removeEventListener('resize', this.onResize)
      }
    }

    componentDidUpdate (oldProps) {
      this.loadSettings(oldProps)
    }

    onResize = () => {
      this.setState({ renderWidth: this.getLayoutWidth() })
    }

    getLayoutWidth () {
      return (document.body && document.body.offsetWidth) || 0
    }

    loadSettings (oldProps) {
      const { settings, request, getNewSettings } = this.props
      const { projectSlug: oldProjectSlug, env: oldEnv } = oldProps.match.params
      const { projectSlug: newProjectSlug, env: newEnv } = this.props.match.params

      if (
        (
          oldProjectSlug !== newProjectSlug ||
          oldEnv !== newEnv
        ) ||
        (
          settings === undefined ||
          (request.networkStatus === 'ready')
        )
      ) {
        getNewSettings()
      }
    }

    render () {
      const { settings, request, history, location, match } = this.props
      const { renderWidth } = this.state

      if (!settings || !request || request.networkStatus === 'loading') {
        return <div />
      }

      if (request.networkStatus === 'failed') {
        return <ErrorBlock {...getErrorMessages(match.params, 'settings')[4044]} fullscreen match={match} />
      }

      return (
        <WrappedComponent
          settings={settings}
          settingsRequest={request}
          history={history}
          location={location}
          match={match}
          renderWidth={renderWidth}
        />
      )
    }
  }

  hoistNonReactStatic(WithSettings, WrappedComponent)

  return connect(mapStateToProps, mapDispatchToProps)(WithSettings)
}

I've managed to narrow it down to the WithSettings HOC that I am using to wrap my route components in. If I don't use the WithSettings HOC (as with the Article route) then my SSR output waits for the async import to complete, and the server generated html includes markup related to the route (good!). If I do use the HOC (as with the Page route) then the module immediately resolves and my SSR output turns into <div id="root"></div because it no longer waits for the dynamic import to complete before rendering. Problem is: I need the WithSettings HOC in all my routes as it fetches required data from the server that I need to render the app.

Can anyone tell me how I can use a HOC and use React Loadable's async component for route components so that it works on the server?

Squrler
  • 3,444
  • 8
  • 41
  • 62
  • Is [this](https://github.com/jamiebuilds/react-loadable#customizing-rendering) what you are looking for? Here you can download the required data files and can be passed to component before rendering it. – Hriday Modi Jun 25 '18 at 19:21
  • Hey @HridayModi, I used that example in my `const Page` in my original question. Unfortunately, using this pattern leads to the problem I described :( – Squrler Jun 25 '18 at 19:26
  • Code looks okay. Can I see the implementation of `WithSettings` HOC? – Hriday Modi Jun 25 '18 at 19:36
  • @HridayModi Added `WithSettings.js` in the question. Thanks for thinking along! – Squrler Jun 25 '18 at 21:14
  • Oh and just to emphasize: this (both the Page and Article routes) work fine on client. React Loadable's loader shows and the page renders completely. It's just that on the server the route that uses the HOC doesn't get rendered. – Squrler Jun 25 '18 at 21:23

1 Answers1

1

Managed to figure it out.

It was due to my HOC. In the render method it would return <div /> when settings or request where undefined or request.networkStatus is loading. It turns out this tripped up server side rendering. Removing this block was enough to make it work.

Squrler
  • 3,444
  • 8
  • 41
  • 62