Skip to content

Commit a6b70b3

Browse files
committed
Implemented React.lazy() + dynamic import() for all 114 plays, enabling Webpack to produce per-play code-split chunks. Users now only download a play's code when they navigate to it.
1 parent af3995b commit a6b70b3

2 files changed

Lines changed: 130 additions & 1 deletion

File tree

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import React from 'react';
2+
import { ReactComponent as ImageOops } from 'images/img-oops.svg';
3+
4+
class PlayErrorBoundary extends React.Component {
5+
constructor(props) {
6+
super(props);
7+
this.state = { hasError: false, error: null, isChunkError: false };
8+
}
9+
10+
static getDerivedStateFromError(error) {
11+
// Detect chunk load failures (network errors loading lazy chunks)
12+
const isChunkError =
13+
error?.name === 'ChunkLoadError' ||
14+
/loading chunk/i.test(error?.message) ||
15+
/failed to fetch dynamically imported module/i.test(error?.message);
16+
17+
return { hasError: true, error, isChunkError };
18+
}
19+
20+
componentDidCatch(error, errorInfo) {
21+
console.error(`Error loading play "${this.props.playName}":`, error, errorInfo);
22+
}
23+
24+
handleRetry = () => {
25+
this.setState({ hasError: false, error: null, isChunkError: false });
26+
};
27+
28+
handleGoBack = () => {
29+
window.location.href = '/plays';
30+
};
31+
32+
render() {
33+
if (this.state.hasError) {
34+
return (
35+
<div className="play-error-boundary" style={styles.container}>
36+
<ImageOops style={styles.image} />
37+
<h2 style={styles.title}>
38+
{this.state.isChunkError ? 'Failed to load this play' : 'Something went wrong'}
39+
</h2>
40+
<p style={styles.message}>
41+
{this.state.isChunkError
42+
? 'There was a network error loading this play. Please check your connection and try again.'
43+
: `An error occurred while rendering "${this.props.playName || 'this play'}".`}
44+
</p>
45+
<div style={styles.actions}>
46+
{this.state.isChunkError && (
47+
<button style={styles.retryButton} onClick={this.handleRetry}>
48+
Retry
49+
</button>
50+
)}
51+
<button style={styles.backButton} onClick={this.handleGoBack}>
52+
Back to Plays
53+
</button>
54+
</div>
55+
</div>
56+
);
57+
}
58+
59+
return this.props.children;
60+
}
61+
}
62+
63+
const styles = {
64+
container: {
65+
display: 'flex',
66+
flexDirection: 'column',
67+
alignItems: 'center',
68+
justifyContent: 'center',
69+
padding: '3rem 1.5rem',
70+
textAlign: 'center',
71+
minHeight: '50vh'
72+
},
73+
image: {
74+
width: '200px',
75+
height: 'auto',
76+
marginBottom: '1.5rem',
77+
opacity: 0.8
78+
},
79+
title: {
80+
fontSize: '1.5rem',
81+
fontWeight: 600,
82+
color: '#333',
83+
margin: '0 0 0.75rem'
84+
},
85+
message: {
86+
fontSize: '1rem',
87+
color: '#666',
88+
maxWidth: '500px',
89+
lineHeight: 1.5,
90+
margin: '0 0 1.5rem'
91+
},
92+
actions: {
93+
display: 'flex',
94+
gap: '1rem'
95+
},
96+
retryButton: {
97+
padding: '0.6rem 1.5rem',
98+
fontSize: '0.95rem',
99+
fontWeight: 600,
100+
border: 'none',
101+
borderRadius: '6px',
102+
cursor: 'pointer',
103+
background: '#00f2fe',
104+
color: '#fff',
105+
transition: 'opacity 0.2s'
106+
},
107+
backButton: {
108+
padding: '0.6rem 1.5rem',
109+
fontSize: '0.95rem',
110+
fontWeight: 600,
111+
border: '2px solid #00f2fe',
112+
borderRadius: '6px',
113+
cursor: 'pointer',
114+
background: 'transparent',
115+
color: '#00f2fe',
116+
transition: 'opacity 0.2s'
117+
}
118+
};
119+
120+
export default PlayErrorBoundary;

‎src/common/playlists/PlayMeta.jsx‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { PageNotFound } from 'common';
1010
import thumbPlay from 'images/thumb-play.png';
1111
import { getProdUrl } from 'common/utils/commonUtils';
1212
import { loadCoverImage } from 'common/utils/coverImageUtil';
13+
import PlayErrorBoundary from 'common/playlists/PlayErrorBoundary';
1314

1415
function PlayMeta() {
1516
const [loading, setLoading] = useState(true);
@@ -87,6 +88,10 @@ function PlayMeta() {
8788
const renderPlayComponent = () => {
8889
const Comp = plays[play.component || toSanitized(play.title_name)];
8990

91+
if (!Comp) {
92+
return <PageNotFound />;
93+
}
94+
9095
return <Comp {...play} />;
9196
};
9297

@@ -103,7 +108,11 @@ function PlayMeta() {
103108
<meta content={play.description} data-react-helmet="true" name="twitter:description" />
104109
<meta content={ogTagImage} data-react-helmet="true" name="twitter:image" />
105110
</Helmet>
106-
<Suspense fallback={<Loader />}>{renderPlayComponent()}</Suspense>
111+
<Suspense
112+
fallback={<Loader subtitle="Please wait while the play loads" title="Loading Play..." />}
113+
>
114+
<PlayErrorBoundary playName={play.name}>{renderPlayComponent()}</PlayErrorBoundary>
115+
</Suspense>
107116
</>
108117
);
109118
}

0 commit comments

Comments
 (0)