Load a feature on demand and show a purposeful fallback.
Read the explanation, run the worked code, then complete the practice lab in Assignments. This advanced lesson includes worked code and a dedicated practice lab.
Understand the idea
A large application need not load every feature before its first screen. A dynamic import gives a bundler an opportunity to split code; React.lazy turns a component module into a component that can be loaded when rendered. Declare lazy components outside other components so their identity remains stable.
Suspense handles a pending lazy module by showing fallback UI at a chosen boundary. Keep navigation and already usable content outside that boundary when they should remain available. The example requires two files: App.jsx and Reports.jsx. The imported module exports its component as default. Loading may be too fast to see locally; use network throttling with a fresh page load to observe it.
A fetch in useEffect does not automatically activate Suspense. Data suspension needs a compatible framework or cache integration. A loading fallback is also not an error boundary: failed module loading needs separate error handling. Test initial loading, a previously loaded module and a failed module request. Do not add artificial delays to production just to make a spinner visible.
Run the example
Use a separate React practice project with a Node.js version supported by Vite. Create it once with these commands, then use its development URL. Do not paste this into the Edupoly website’s own source files.
npm create vite@latest react-practice -- --template react
cd react-practice
npm install
npm run dev
Replace src/App.jsx with the following complete example. Keep Vite’s generated src/main.jsx entry point.
Create the additional file src/Reports.jsx shown below alongside this example.
src/App.jsx
import { lazy, Suspense, useState } from 'react';
const Reports = lazy(() => import('./Reports.jsx'));
export default function App() {
const [open, setOpen] = useState(false);
return <main><h1>Student dashboard</h1><button onClick={() => setOpen(true)}>Open reports</button>
{open && <Suspense fallback={<p role="status">Loading reports…</p>}><Reports /></Suspense>}
</main>;
}
Expected result
The dashboard opens first. Open reports requests the Reports module and then shows its content. A fallback may be visible on a slow first load.
src/Reports.jsx
export default function Reports() {
return <section><h2>Student reports</h2><p>Attendance: 92%</p><p>Assignments completed: 18</p></section>;
}
Apply this to your assignments
Create both files, inspect the Network panel, and open the reports feature. Repeat with throttling after reloading. Add a boundary around a second optional feature and explain why you chose separate or shared fallback regions.
Practice starting point
Use the worked example above as your starting point.
Open Assignments for this lesson’s practice lab and acceptance checks.
Common mistakes
Do not call lazy inside App. Do not assume Suspense detects an ordinary fetch effect or catches a rejected import as a loading state forever.
Check your understanding
- Predict the output before running the example, then compare it with the result panel.
- Explain which value each expression reads and which action changes it.
- Try the boundary cases listed in the practice task and record what you observe.
- Rebuild one matching assignment without copying, then compare behavior with the original example.
Reference
Official documentation for this topic · Vite setup requirements
