Skip to content

Commit 31532ff

Browse files
authored
Dynamic loading optimizations (#1925)
* dynamic: React QR Scanner, React Datepicker; placeholder for syntax highlighting * Loading placeholders, prevent layout shifting
1 parent 31b58ba commit 31532ff

File tree

3 files changed

+138
-80
lines changed

3 files changed

+138
-80
lines changed

components/form.js

Lines changed: 78 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import TextareaAutosize from 'react-textarea-autosize'
2020
import { useToast } from './toast'
2121
import { numWithUnits } from '@/lib/format'
2222
import textAreaCaret from 'textarea-caret'
23-
import ReactDatePicker from 'react-datepicker'
2423
import 'react-datepicker/dist/react-datepicker.css'
2524
import useDebounceCallback, { debounce } from './use-debounce-callback'
2625
import { FileUpload } from './file-upload'
@@ -38,9 +37,10 @@ import QrIcon from '@/svgs/qr-code-line.svg'
3837
import QrScanIcon from '@/svgs/qr-scan-line.svg'
3938
import { useShowModal } from './modal'
4039
import { QRCodeSVG } from 'qrcode.react'
41-
import { Scanner } from '@yudiel/react-qr-scanner'
40+
import dynamic from 'next/dynamic'
4241
import { qrImageSettings } from './qr'
4342
import { useIsClient } from './use-client'
43+
import PageLoading from './page-loading'
4444

4545
export class SessionRequiredError extends Error {
4646
constructor () {
@@ -971,6 +971,19 @@ export function Select ({ label, items, info, groupClassName, onChange, noForm,
971971
)
972972
}
973973

974+
function DatePickerSkeleton () {
975+
return (
976+
<div className='react-datepicker-wrapper'>
977+
<input className='form-control clouds fade-out p-0 px-2 mb-0' />
978+
</div>
979+
)
980+
}
981+
982+
const ReactDatePicker = dynamic(() => import('react-datepicker').then(mod => mod.default), {
983+
ssr: false,
984+
loading: () => <DatePickerSkeleton />
985+
})
986+
974987
export function DatePicker ({ fromName, toName, noForm, onChange, when, from, to, className, ...props }) {
975988
const formik = noForm ? null : useFormikContext()
976989
const [,, fromHelpers] = noForm ? [{}, {}, {}] : useField({ ...props, name: fromName })
@@ -1038,19 +1051,23 @@ export function DatePicker ({ fromName, toName, noForm, onChange, when, from, to
10381051
}
10391052

10401053
return (
1041-
<ReactDatePicker
1042-
className={`form-control text-center ${className}`}
1043-
selectsRange
1044-
maxDate={new Date()}
1045-
minDate={new Date('2021-05-01')}
1046-
{...props}
1047-
selected={new Date(innerFrom)}
1048-
startDate={new Date(innerFrom)}
1049-
endDate={innerTo ? new Date(innerTo) : undefined}
1050-
dateFormat={dateFormat}
1051-
onChangeRaw={onChangeRawHandler}
1052-
onChange={innerOnChange}
1053-
/>
1054+
<>
1055+
{ReactDatePicker && (
1056+
<ReactDatePicker
1057+
className={`form-control text-center ${className}`}
1058+
selectsRange
1059+
maxDate={new Date()}
1060+
minDate={new Date('2021-05-01')}
1061+
{...props}
1062+
selected={new Date(innerFrom)}
1063+
startDate={new Date(innerFrom)}
1064+
endDate={innerTo ? new Date(innerTo) : undefined}
1065+
dateFormat={dateFormat}
1066+
onChangeRaw={onChangeRawHandler}
1067+
onChange={innerOnChange}
1068+
/>
1069+
)}
1070+
</>
10541071
)
10551072
}
10561073

@@ -1070,19 +1087,27 @@ export function DateTimeInput ({ label, groupClassName, name, ...props }) {
10701087

10711088
function DateTimePicker ({ name, className, ...props }) {
10721089
const [field, , helpers] = useField({ ...props, name })
1090+
const ReactDatePicker = dynamic(() => import('react-datepicker').then(mod => mod.default), {
1091+
ssr: false,
1092+
loading: () => <span>loading date picker</span>
1093+
})
10731094
return (
1074-
<ReactDatePicker
1075-
{...field}
1076-
{...props}
1077-
showTimeSelect
1078-
dateFormat='Pp'
1079-
className={`form-control ${className}`}
1080-
selected={(field.value && new Date(field.value)) || null}
1081-
value={(field.value && new Date(field.value)) || null}
1082-
onChange={(val) => {
1083-
helpers.setValue(val)
1084-
}}
1085-
/>
1095+
<>
1096+
{ReactDatePicker && (
1097+
<ReactDatePicker
1098+
{...field}
1099+
{...props}
1100+
showTimeSelect
1101+
dateFormat='Pp'
1102+
className={`form-control ${className}`}
1103+
selected={(field.value && new Date(field.value)) || null}
1104+
value={(field.value && new Date(field.value)) || null}
1105+
onChange={(val) => {
1106+
helpers.setValue(val)
1107+
}}
1108+
/>
1109+
)}
1110+
</>
10861111
)
10871112
}
10881113

@@ -1149,6 +1174,10 @@ function QrPassword ({ value }) {
11491174
function PasswordScanner ({ onScan, text }) {
11501175
const showModal = useShowModal()
11511176
const toaster = useToast()
1177+
const Scanner = dynamic(() => import('@yudiel/react-qr-scanner').then(mod => mod.Scanner), {
1178+
ssr: false,
1179+
loading: () => <PageLoading />
1180+
})
11521181

11531182
return (
11541183
<InputGroup.Text
@@ -1158,26 +1187,28 @@ function PasswordScanner ({ onScan, text }) {
11581187
return (
11591188
<div>
11601189
{text && <h5 className='line-height-md mb-4 text-center'>{text}</h5>}
1161-
<Scanner
1162-
formats={['qr_code']}
1163-
onScan={([{ rawValue: result }]) => {
1164-
onScan(result)
1165-
onClose()
1166-
}}
1167-
styles={{
1168-
video: {
1169-
aspectRatio: '1 / 1'
1170-
}
1171-
}}
1172-
onError={(error) => {
1173-
if (error instanceof DOMException) {
1174-
console.log(error)
1175-
} else {
1176-
toaster.danger('qr scan: ' + error?.message || error?.toString?.())
1177-
}
1178-
onClose()
1179-
}}
1180-
/>
1190+
{Scanner && (
1191+
<Scanner
1192+
formats={['qr_code']}
1193+
onScan={([{ rawValue: result }]) => {
1194+
onScan(result)
1195+
onClose()
1196+
}}
1197+
styles={{
1198+
video: {
1199+
aspectRatio: '1 / 1'
1200+
}
1201+
}}
1202+
onError={(error) => {
1203+
if (error instanceof DOMException) {
1204+
console.log(error)
1205+
} else {
1206+
toaster.danger('qr scan: ' + error?.message || error?.toString?.())
1207+
}
1208+
onClose()
1209+
}}
1210+
/>
1211+
)}
11811212
</div>
11821213
)
11831214
})

components/text.js

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -238,14 +238,28 @@ function Table ({ node, ...props }) {
238238
)
239239
}
240240

241+
// prevent layout shifting when the code block is loading
242+
function CodeSkeleton ({ className, children, ...props }) {
243+
return (
244+
<div className='rounded' style={{ padding: '0.5em' }}>
245+
<code className={`${className}`} {...props}>
246+
{children}
247+
</code>
248+
</div>
249+
)
250+
}
251+
241252
function Code ({ node, inline, className, children, style, ...props }) {
242253
const [ReactSyntaxHighlighter, setReactSyntaxHighlighter] = useState(null)
243254
const [syntaxTheme, setSyntaxTheme] = useState(null)
244255
const language = className?.match(/language-(\w+)/)?.[1] || 'text'
245256

246257
const loadHighlighter = useCallback(() =>
247258
Promise.all([
248-
dynamic(() => import('react-syntax-highlighter').then(mod => mod.LightAsync), { ssr: false }),
259+
dynamic(() => import('react-syntax-highlighter').then(mod => mod.LightAsync), {
260+
ssr: false,
261+
loading: () => <CodeSkeleton className={className} {...props}>{children}</CodeSkeleton>
262+
}),
249263
import('react-syntax-highlighter/dist/cjs/styles/hljs/atom-one-dark').then(mod => mod.default)
250264
]), []
251265
)
@@ -260,7 +274,7 @@ function Code ({ node, inline, className, children, style, ...props }) {
260274
}
261275
}, [inline])
262276

263-
if (inline || !ReactSyntaxHighlighter || !syntaxTheme) {
277+
if (inline || !ReactSyntaxHighlighter) { // inline code doesn't have a border radius
264278
return (
265279
<code className={className} {...props}>
266280
{children}
@@ -269,9 +283,13 @@ function Code ({ node, inline, className, children, style, ...props }) {
269283
}
270284

271285
return (
272-
<ReactSyntaxHighlighter style={syntaxTheme} language={language} PreTag='div' customStyle={{ borderRadius: '0.3rem' }} {...props}>
273-
{children}
274-
</ReactSyntaxHighlighter>
286+
<>
287+
{ReactSyntaxHighlighter && syntaxTheme && (
288+
<ReactSyntaxHighlighter style={syntaxTheme} language={language} PreTag='div' customStyle={{ borderRadius: '0.3rem' }} {...props}>
289+
{children}
290+
</ReactSyntaxHighlighter>
291+
)}
292+
</>
275293
)
276294
}
277295

pages/withdraw.js

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import { lnAddrSchema, withdrawlSchema } from '@/lib/validate'
1515
import { useShowModal } from '@/components/modal'
1616
import { useField } from 'formik'
1717
import { useToast } from '@/components/toast'
18-
import { Scanner } from '@yudiel/react-qr-scanner'
1918
import { decode } from 'bolt11'
2019
import CameraIcon from '@/svgs/camera-line.svg'
2120
import { FAST_POLL_INTERVAL, SSR } from '@/lib/constants'
@@ -24,6 +23,8 @@ import useDebounceCallback from '@/components/use-debounce-callback'
2423
import { lnAddrOptions } from '@/lib/lnurl'
2524
import AccordianItem from '@/components/accordian-item'
2625
import { numWithUnits } from '@/lib/format'
26+
import PageLoading from '@/components/page-loading'
27+
import dynamic from 'next/dynamic'
2728

2829
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
2930

@@ -153,39 +154,47 @@ function InvoiceScanner ({ fieldName }) {
153154
const showModal = useShowModal()
154155
const [,, helpers] = useField(fieldName)
155156
const toaster = useToast()
157+
const Scanner = dynamic(() => import('@yudiel/react-qr-scanner').then(mod => mod.Scanner), {
158+
ssr: false,
159+
loading: () => <PageLoading />
160+
})
161+
156162
return (
157163
<InputGroup.Text
158164
style={{ cursor: 'pointer' }}
159165
onClick={() => {
160166
showModal(onClose => {
161167
return (
162-
<Scanner
163-
formats={['qr_code']}
164-
onScan={([{ rawValue: result }]) => {
165-
result = result.toLowerCase()
166-
if (result.split('lightning=')[1]) {
167-
helpers.setValue(result.split('lightning=')[1].split(/[&?]/)[0])
168-
} else if (decode(result.replace(/^lightning:/, ''))) {
169-
helpers.setValue(result.replace(/^lightning:/, ''))
170-
} else {
171-
throw new Error('Not a proper lightning payment request')
172-
}
173-
onClose()
174-
}}
175-
styles={{
176-
video: {
177-
aspectRatio: '1 / 1'
178-
}
179-
}}
180-
onError={(error) => {
181-
if (error instanceof DOMException) {
182-
console.log(error)
183-
} else {
184-
toaster.danger('qr scan: ' + error?.message || error?.toString?.())
185-
}
186-
onClose()
187-
}}
188-
/>
168+
<>
169+
{Scanner && (
170+
<Scanner
171+
formats={['qr_code']}
172+
onScan={([{ rawValue: result }]) => {
173+
result = result.toLowerCase()
174+
if (result.split('lightning=')[1]) {
175+
helpers.setValue(result.split('lightning=')[1].split(/[&?]/)[0])
176+
} else if (decode(result.replace(/^lightning:/, ''))) {
177+
helpers.setValue(result.replace(/^lightning:/, ''))
178+
} else {
179+
throw new Error('Not a proper lightning payment request')
180+
}
181+
onClose()
182+
}}
183+
styles={{
184+
video: {
185+
aspectRatio: '1 / 1'
186+
}
187+
}}
188+
onError={(error) => {
189+
if (error instanceof DOMException) {
190+
console.log(error)
191+
} else {
192+
toaster.danger('qr scan: ' + error?.message || error?.toString?.())
193+
}
194+
onClose()
195+
}}
196+
/>)}
197+
</>
189198
)
190199
})
191200
}}

0 commit comments

Comments
 (0)