shell-safe-imports
The <DappsShell /> is a lazy-loaded chunk. When a user navigates to /events, /blog, /jump, /social, /cast, /storage, or /profile, the chunk loads — and every module in its import graph evaluates synchronously.
If ANY one of those modules has a throw at the top level (not inside a function — at module scope), the entire lazy chunk load fails. Not just the feature that needed the validation. All of DappsShell crashes, the user sees a blank page or fallback UI for all heavy routes, even ones unrelated to the broken module.
Rule 16
NEVER throw at module top-level in files imported by src/shells/store.ts or any file reachable from the lazy DappsShell chunk boundary.
Pattern — lazy validation getter
For env-var validation (the common case that tempts a top-level throw), use a lazy getter that throws only on invocation:
const getCmsBaseUrl = (): string => {
const url = getEnv('CMS_BASE_URL')
if (!url) throw new Error('CMS_BASE_URL is required')
return url
}
// Inside endpoint:
url: `${getCmsBaseUrl()}/entries?...`
The error surfaces at request time (the endpoint that needs the var), not at chunk-load time (which crashes everything).
Anti-patterns
// 🚫 At module top-level — crashes the entire DappsShell on import
const CMS_BASE_URL = getEnv('CMS_BASE_URL')
if (!CMS_BASE_URL) throw new Error('CMS_BASE_URL is required')
// 🚫 Class field default that evaluates synchronously
class Client {
baseUrl =
getEnv('CMS_BASE_URL') ??
(() => {
throw new Error('missing')
})()
}
// 🚫 IIFE at top-level
const baseUrl = (() => {
const url = getEnv('CMS_BASE_URL')
if (!url) throw new Error('CMS_BASE_URL is required')
return url
})()
// ✅ Lazy getter — only throws if the endpoint that needs it is actually called
const getCmsBaseUrl = () => {
const url = getEnv('CMS_BASE_URL')
if (!url) throw new Error('CMS_BASE_URL is required')
return url
}
Which files are shell-reachable?
Conservatively, treat any of these as reachable:
src/shells/store.tsand anything it imports directly (listener middleware, base clients).src/services/*Client.ts— all RTK Query base clients (registered instore.ts).src/features/*/*.client.tsand siblings (*.admin.client.ts,*.search.client.ts) — injected at store init.- Helpers imported by any of the above (sanitizers, mappers, utility modules).
If you're unsure, assume yes. The cost of using a lazy getter when it wasn't strictly needed is zero. The cost of a top-level throw in a reachable module is a broken DappsShell.
Verification
After editing any shell-reachable file:
npm run build && npm run preview
Then navigate to /events or /blog and confirm the route loads (not the fallback). If the route silently fails, check the console — top-level throws appear as Failed to load resource / chunk-load errors.
Related
- RTK Query patterns → skill
rtk-query-split. - Adding a new feature to the heavy tier → skill
add-route(heavy steps).