I have a (Server Side Rendered)SSR application built using React & Redux. I also use the loadable library. The application is hosted on Heroku and uses Cloudflare caching.
The application intermittently ends up in a 500 error on Heroku and I am not able to find the root cause of the error. Below is the error message
loadable-components: failed to synchronously load component, which expected to be available {
fileName: 592,
chunkName: 'home-route',
error: 'Cannot convert undefined or null to object'
}
TypeError: Cannot convert undefined or null to object
at getPrototypeOf (<anonymous>)
at hoistNonReactStatics (/app/node_modules/@loadable/component/node_modules/hoist-non-react-statics/dist/hoist-non-react-statics.cjs.js:70:32)
at resolve (/app/node_modules/@loadable/component/dist/loadable.cjs.js:128:7)
at InnerLoadable.loadSync (/app/node_modules/@loadable/component/dist/loadable.cjs.js:279:24)
at new InnerLoadable (/app/node_modules/@loadable/component/dist/loadable.cjs.js:173:17)
at d (/app/node_modules/react-dom/cjs/react-dom-server.node.production.min.js:36:320)
at $a (/app/node_modules/react-dom/cjs/react-dom-server.node.production.min.js:39:16)
at a.b.render (/app/node_modules/react-dom/cjs/react-dom-server.node.production.min.js:44:476)
at a.b.read (/app/node_modules/react-dom/cjs/react-dom-server.node.production.min.js:44:18)
at Object.renderToString (/app/node_modules/react-dom/cjs/react-dom-server.node.production.min.js:54:364)
at /app/static/dist/server/server.js:79236:42
at runMicrotasks (<anonymous>)
at processTicksAndRejections (internal/process/task_queues.js:97:5)
Since this is happening intermittently, I am not able to find the root cause of why this is happening. Below are the details for the Home Component where the error occurs
src/routes/HomeRoute.js
// Libs
import { asyncConnectWithModifications } from 'decorators/asyncConnectWithModifications'
import { compose } from 'redux'
import { connect } from 'react-redux'
import path from 'ramda/src/path'
import loadable from '@loadable/component'
// Actions
import { getTotalCampers } from 'myredux/modules/search'
import { getPagesList } from 'myredux/modules/content'
// Constants
import defaultCoords from 'routes/Search/defaultCoords'
// Components
const Home = loadable(() => import(/* webpackChunkName: "home-route" */ './Home'))
export default compose(
asyncConnectWithModifications({
getPromise: ({ store: { dispatch, getState } }) => {
const state = getState()
const totalCampers = path(['search', 'totalCampersSearchResult', 'Pagination', 'Total'], state)
const totalCampersSearch = path(['search', 'searchResult', 'Pagination', 'Total'], state)
const promises = []
if (!totalCampers || (totalCampersSearch > totalCampers)) {
// Set default coords
const { centerLat, centerLng } = defaultCoords[state.app.country.code] || {}
promises.push(dispatch(getTotalCampers({ center_lat: centerLat, center_lng: centerLng })))
}
promises.push(dispatch(
getPagesList('posts'),
))
return Promise.all(promises)
},
}),
connect(state => ({
locale: state.app.locale,
})),
)(Home)
src/routes/Home.js
...
render() {
const { app, env, search, content, locale, translate, formatNumber, lastDraftCamperId, userId } = this.props
const { bannerCamperClosed } = this.state
const totalCampers = path(['totalCampersSearchResult', 'Pagination', 'Total'], search)
const formattedTotalCampers = formatNumber(totalCampers)
// Canonical for home page is current domain
const canonical = `https://${app.domain.name}/`
const meta = {
title: translate({ id: 'home.pageTitle' }),
description: translate({ id: 'home.pageDescription' }),
link: [
{ rel: 'canonical', href: canonical },
{ rel: 'alternate', href: 'https://paulcamper.de/', hrefLang: 'de-DE' },
{ rel: 'alternate', href: 'https://paulcamper.at/', hrefLang: 'de-AT' },
// { rel: 'alternate', href: 'https://paulcamper.co.uk/', hrefLang: 'en-GB' }, // UK domain is disabled for now [.co.uk uncomment]
// { rel: 'alternate', href: 'https://paulcamper.es/', hrefLang: 'es-ES' }, // ES domain is disabled for now [.es uncomment]
// { rel: 'alternate', href: 'https://paulcamper.it/', hrefLang: 'it-IT' }, // Italian domain is disabled for now [.it uncomment]
{ rel: 'alternate', href: 'https://paulcamper.nl/', hrefLang: 'nl-NL' },
],
}
const numberOfMonths = app.breakpoint === 'xs' ? 1 : 2
const { DE } = LOCALE_LANGUAGES
const { ES, FR, IT } = LOCALE_CODES
const doHideHomeCities = [ES, FR, IT].includes(locale.code)
return (
<NestedStatus maxage={DAY}>
<div className={styles.container}>
<DocumentMeta {...meta}/>
<BlockedLenderBanner/>
<BodyClassName className="page-home"/>
{!bannerCamperClosed && lastDraftCamperId &&
<HomeBannerCamper
camperId={lastDraftCamperId}
userId={userId}
onClose={this.handleBannerCamperClose}
/>}
<div className={styles.head} style={{ backgroundImage: `url(${this.bgImage})` }}>
<div className={styles.headContent}>
<div className={styles.header}>
<Header
theme="light"
pageName="home"
isHomePage
isLenderButtonActive
isSearchButtonActive={false}
/>
</div>
<Grid>
<Row>
<Col xs={12}>
<div className={styles.title}>
<Row>
<Col xs={12} md={10} mdOffset={1} lg={8} lgOffset={2}>
<h1 className={styles.headTitle}>
<FormattedMessage id="home.v2.title.xp"/>
</h1>
{app.locale.language === 'nl' &&
<h2 className={styles.headSubTitle}>
<FormattedMessage id="home.subTitle"/>
</h2>}
<TrustBoxNew
withoutReviews
white
small
/>
</Col>
</Row>
</div>
</Col>
</Row>
</Grid>
<Grid>
<Row>
<Col xs={12}>
<div className={styles.search}>
<SearchFormHome
locale={locale}
numberOfMonths={numberOfMonths}
filters={search.filters}
onApply={this.handleSearchFormSubmit}
onLocationChange={this.handleLocationChange}
onFiltersChange={this.props.setFilters}
onSubmit={this.handleSearchFormSubmit}
withAutoFocus={false}
submitLocationOnBlur
/>
</div>
<div className={styles.publicRequest}>
<FormattedHTMLMessage id="home.publicRequests" values={{ total: formattedTotalCampers }}/>
{' '}
<Link to="/add-public-request/" onClick={this.handlePublickRequest}>
<FormattedMessage id="home.publicRequests.create"/>
<i className={styles.publicRequestIcon}>
<Icon src={require('icons/icon-arrow-left.svg')}/>
</i>
</Link>
</div>
</Col>
</Row>
</Grid>
{/** app.locale.language === 'de' &&
<div className={styles.firstPlaceBanner}>
<FirstPlaceBanner/>
</div> **/}
</div>
</div>
<SearchWizardTest component={(
<SearchWizardCta locale={locale} onClick={this.handleSearchWizardClick}/>
)}
/>
<div className={styles.trust}>
<HomeTrust app={app}/>
</div>
<PromiseBlock/>
<AwarenessBlock>
{locale.code === LOCALE_CODES.DE &&
<AwarenessBlockItem
buttonText="Ja, gerne"
onButtonClick={this.handleRecruitingAwareClick}
preset={2}
text="Wir führen regelmäßig Umfragen zur Website, zu Camping und Reisethemen durch. Bist du dabei?"
title="Lass uns PaulCamper noch besser machen"
trackName="test-user-recruiting"
trackPage="home"
/>}
<AwarenessBlockItem
buttonText={translate({ id: 'home.awareness.friendly-covid.buttonText' })}
onButtonClick={this.handleAwarenessFCClick}
preset={3}
text={translate({ id: 'home.awareness.friendly-covid.text' })}
title={translate({ id: 'home.awareness.friendly-covid.title' })}
trackName="friendly-covid-cancel"
trackPage="home"
/>
</AwarenessBlock>
<HomeHowPaulcamperWorks
locale={locale}
totalCampers={formattedTotalCampers}
/>
<div className={styles.renterSteps}>
<HomeRenterSteps/>
</div>
{!doHideHomeCities && // temporarily hidden section for .it, co.uk, .es domains [.it uncomment][.es uncomment][.co.uk uncomment]
<Grid>
<Row>
<Col xs={12}>
<div className={styles.cities}>
<HomeCities country={app.country.code} locale={locale}/>
</div>
</Col>
</Row>
</Grid>}
<div className={styles.community}>
<HomeCommunity locale={locale}/>
</div>
<div className={styles.sectionText}>
<Grid>
<Row>
<Col xs={12}>
<FormattedHTMLMessage id="home.text1"/>
</Col>
</Row>
</Grid>
</div>
{locale.language === DE &&
<div className={styles.posts}>
<Grid>
<Row>
<Col xs={12}>
<HomePosts breakpoint={app.breakpoint} locale={locale} posts={content.pagesList}/>
</Col>
</Row>
</Grid>
</div>}
<div className={styles.contacts}>
<Grid>
<Row>
<Col xs={12}>
<SectionQuestions isLenderView/>
</Col>
</Row>
</Grid>
</div>
<div className={styles.team}>
<HomeTeam locale={locale}/>
</div>
<div className={styles.sectionText}>
<Grid>
<Row>
<Col xs={12}>
<FormattedHTMLMessage id="home.text2"/>
</Col>
</Row>
</Grid>
</div>
<Footer location={this.props.location} trustbox={false}/>
{env.AUTOTEST_MODE !== 'true' &&
// eslint-disable-next-line react/no-danger
<script defer dangerouslySetInnerHTML={{ __html: customerSupportWidget(locale.code) }}/>}
</div>
</NestedStatus>
)
}
}
export default compose(
connect(
state => ({
app: state.app,
auth: state.auth,
env: getEnv(state),
search: state.search,
content: state.content,
fromDate: state.search.fromDate,
lastDraftCamperId: lastDraftCamperIdSelector(state),
userId: userIdSelector(state),
locale: state.app.locale,
tillDate: state.search.tillDate,
}),
{ setFilter, setFilters, setLocation }),
withStyles(styles),
intl(),
)(Home)
src/server/createRenderApp.js
...
// Critical CSS
const css = new Set()
// Global (context) variables that can be easily accessed from any React component
// https://facebook.github.io/react/docs/context.html
const appContext = {
// Enables critical path CSS rendering
// https://github.com/kriasoft/isomorphic-style-loader
insertCss: (...styles) => styles.forEach(style => css.add(style._getCss())),
}
const url = req.originalUrl || req.url
// fix query and search
const location = parseUrl(url, true)
const routes = getRoutes(store)
const helpers = { client }
loadOnServer({
store,
location,
routes,
helpers,
})
.then(() => {
appContext.translations = translations[domainConfig.locale]
const routerContext = {}
const component = (
<App context={appContext}>
<Provider store={store} key="provider">
<ThemeProvider>
<StaticRouter location={location} context={routerContext}>
<ReduxAsyncConnect helpers={helpers} routes={routes} />
</StaticRouter>
</ThemeProvider>
</Provider>
</App>
)
...
Please find the screenshot of the error captured on Sentry here.
Any thoughts or suggestion will definitely help. Thank you in advance.
I was running into the same issue and it turned out that the
getPrototypeOf()
was being called on the anonymous function (see top of the stack traceat getPrototypeOf (<anonymous>)
).In my case, this was happening because I had a
forwardRef
on my component I was using in loadable (equivalent to yourHome
component). This was what was making it an anonymous function. I'm guessing there is a difference between NodeJS and browser JS. This was on loadable components version 5.13. Can't tell if you're using aforwardRef
or not, as the beginning of yourHome
component is missing.Very possible your issue was a different cause, but the solution may be the same. I ended up being setting SSR to false in the loadable component like so: