IT Asset Management Dashboard
Build a multi-page React app with charts, routing, and Redux state.
Introduction
30 Second Summary
Every company tracks its laptops, monitors, and servers somewhere. Usually it starts as a spreadsheet, then grows into a dedicated tool with dashboards, search, and the ability to add or update records on the fly.
In this project, you will build an IT Asset Management Dashboard using React. You will create a multi-page app with interactive charts, a data table, and a modal form that adds and edits assets in a global store.
What You'll Build
Picture clicking between a Dashboard covered in colorful bar and pie charts and an Assets page where you can add a new laptop, edit its status, navigate to another page, and find your changes still waiting when you come back.
By the end of this project, you'll have:
- A sidebar navigation that highlights the active page as you click between Dashboard and Assets, powered by React Router.
- A Dashboard page with a Recharts bar chart showing assets by category and a pie chart showing asset status breakdown.
- An Assets page with a reusable table component displaying mock IT asset data, plus an add/edit modal form that creates and updates assets in a global Redux Toolkit store so your changes survive page navigation.
- Secret Mission: Add search and filter functionality to the Assets table so you can find specific assets instantly as the list grows.
Are there any prerequisites?
You should have basic React knowledge (components, props, and state). You also need Node.js installed on your machine.
Before We Start
Before you touch any code, take a moment to lock in what you're about to build and why it matters.
Scaffold the React App
Every React application starts with a scaffold. Before you build any features, you need a working project with all the right libraries installed so the rest of the project has a solid foundation.
In this step, you'll use Vite to create a React project, then install libraries for routing, state management, and charting.
In this step, get ready to:
- Scaffold a React project using Vite.
- Install libraries for routing, state management, and charting.
- Start the dev server and verify the app runs.
Scaffold the project with Vite
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- Open the integrated terminal by pressing Ctrl+` (backtick) on both macOS and Windows.
- Move to your Desktop by running this command:
cd ~/Desktop
- Scaffold a new React project by running the command below. Vite will prompt you to make two choices.
npm create vite@latest asset-manager
- If npm asks Ok to proceed? (y), type y and press Enter.
- When Vite asks you to select a framework, use the arrow keys to highlight React and press Enter.
- When Vite asks for a variant, select JavaScript and press Enter.
What does this command do?
npm create vite@latest runs the Vite project scaffolder. It generates a ready-to-use React project folder with a dev server, a build pipeline, and sample starter files.
Vite prints a short confirmation showing the project name and the commands to run next.
- Move into the new project folder and install the base dependencies by running these commands:
cd asset-manager
npm install
What does npm install do?
npm install reads the package.json file that Vite generated and downloads every package the project depends on (React, ReactDOM, etc.) into a node_modules folder.
- You should see a summary of added packages in the terminal output. If you see a line like added XXX packages, the install succeeded.
Seeing an error during npm install?
- Make sure you ran cd asset-manager first. Running npm install outside the project folder means there is no package.json to read.
- Check your Node.js version by running node --version. Vite requires Node.js 20.19 or higher.
- Help me troubleshoot my npm install error.
Now open the asset-manager folder in VS Code so you can see the project files in the sidebar.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the asset-manager folder, and click Open.
- If VS Code asks whether you trust the authors of the files, click Yes, I trust the authors.
You should see the project file tree in the left sidebar, including folders like src, public, and files like package.json.
Install the project dependencies
The Vite scaffold gives you a bare React app. You still need the libraries that power routing, global state, and charts.
- Open a new terminal in VS Code by pressing Ctrl+` (backtick).
- Install all five project libraries by running this command:
npm install react-router@7 @reduxjs/toolkit react-redux recharts react-is
What are these packages?
- react-router@7 provides client-side routing so users can navigate between pages without a full page reload.
- @reduxjs/toolkit is Redux Toolkit, a library for managing global state that lives outside any single component.
- react-redux connects Redux Toolkit's store to your React components.
- recharts is Recharts, a charting library built with React components for bar charts, pie charts, and more.
- react-is is a peer dependency that Recharts needs to work with your version of React.
- Wait for the install to finish. You should see another added XXX packages summary in the terminal.
Install failed or showing warnings?
- Peer dependency warnings (yellow text) are safe to ignore. They do not prevent the packages from working.
- If you see a red ERR! line, check that your terminal is inside the asset-manager folder. Run pwd to confirm.
- Help me fix my npm install error for the project dependencies.
Start the dev server and verify
Everything is installed. Time to see if the app actually runs.
- Start the Vite development server by running this command:
npm run dev
What does npm run dev do?
This starts a local development server that watches your files for changes. Every time you save a file, Vite automatically refreshes the browser so you see updates instantly.
The server takes over this terminal window. Leave it running for the rest of the project. When you need to run another command later, you'll open a second terminal.
Before you open the browser, what do you think you'll see at the URL Vite printed? A blank page, an error, or something else?
- Open your browser and go to http://localhost:5173.
You should see the Vite + React starter page with the Vite and React logos and a counter button. This is the default template that Vite scaffolded for you.
Don't see the starter page?
- Double-check the URL. Vite's default port is 5173, not 3000. Look at the terminal output for the exact URL if Vite chose a different port.
- Make sure the npm run dev terminal is still running. If you closed it or pressed Ctrl+C, the server stopped.
- Help me troubleshoot why my Vite dev server page is not loading.
Your project is scaffolded and running. Next up, you'll build the app shell with a sidebar and routing so users can navigate between pages.
Build the App Shell with Sidebar and Routing
Your React project is scaffolded and the dev server is running. Right now, the app is just one static page. A real dashboard needs multiple views that users can switch between.
In this step, you will build the app's layout shell. A Sidebar on the left handles navigation. A Navbar across the top shows the current page title. React Router swaps the page content when the user clicks a link, without a full page reload.
In this step, get ready to:
- Create Sidebar and Navbar layout components with page placeholders.
- Configure React Router for multi-page navigation.
- Style the sidebar layout with CSS.
Create the layout components and page placeholder components
The app needs two pages (Dashboard and Assets) and two layout components (Sidebar for navigation, Navbar for the page title). You will create all of these before wiring them into the router.
- Your dev server is using the current terminal. Open a second terminal in VS Code by pressing Cmd+Shift+` (macOS) or Ctrl+Shift+` (Windows).
- Create the folder structure for your components and pages by running these commands:
mkdir src/pages
mkdir src/components
- In VS Code's Explorer sidebar, right-click the src/pages folder.
- Click New File and name it Dashboard.jsx.
- Paste in this placeholder code:
// Placeholder page rendered on the "/" route
function Dashboard() {
return <div><h2>Dashboard</h2></div>
}
export default Dashboard
- Create another new file in src/pages named Assets.jsx and paste in this code:
// Placeholder page rendered on the "/assets" route
function Assets() {
return <div><h2>Assets</h2></div>
}
export default Assets
- In VS Code's Explorer sidebar, expand src/pages. You should see Dashboard.jsx and Assets.jsx listed.
What are these placeholder components?
Each page in your app is a React component. Right now they just render a heading so you can confirm routing works. You will replace the placeholder content with real UI in later steps.
- Create a new file in src/components named Sidebar.jsx and paste in this code:
import { NavLink } from 'react-router'
import './Sidebar.css'
function Sidebar() {
return (
<aside className="sidebar">
<div className="sidebar-header">
<h2>AssetTrack</h2>
</div>
<nav className="sidebar-nav">
<NavLink
to="/"
end
className={({ isActive }) =>
'nav-item' + (isActive ? ' active' : '')
}
>
Dashboard
</NavLink>
<NavLink
to="/assets"
className={({ isActive }) =>
'nav-item' + (isActive ? ' active' : '')
}
>
Assets
</NavLink>
</nav>
</aside>
)
}
export default Sidebar
What does NavLink do?
NavLink is a special link component from React Router. It works like a regular <a> tag, but it knows whether it points to the currently active route.
The className prop receives a function with an isActive boolean. When the link matches the current URL, isActive is true and the active CSS class is appended. The end prop on the Dashboard link ensures it only highlights on an exact / match, not on every route that starts with /.
- Create Sidebar.css in src/components and start with the container and header styles:
/* Fixed sidebar pinned to the left edge of the viewport */
.sidebar {
width: 240px;
height: 100vh;
background-color: #2d3436;
color: #ffffff;
position: fixed;
top: 0;
left: 0;
display: flex;
flex-direction: column;
}
/* App name banner with a subtle bottom border */
.sidebar-header {
padding: 20px;
border-bottom: 1px solid #636e72;
}
.sidebar-header h2 {
font-size: 1.4rem;
}
/* Vertical stack for navigation links */
.sidebar-nav {
display: flex;
flex-direction: column;
padding: 12px 0;
}
- In the Explorer sidebar, expand src/components. You should see Sidebar.jsx and Sidebar.css listed.
- Still in Sidebar.css, add the navigation item styles below the existing code:
/* Base style for each navigation link */
.nav-item {
padding: 12px 20px;
color: #b2bec3;
transition: background-color 0.2s, color 0.2s;
}
.nav-item:hover {
background-color: #636e72;
color: #ffffff;
}
/* Highlight applied automatically by NavLink */
.nav-item.active {
background-color: #0984e3;
color: #ffffff;
}
How does the active highlight work?
When NavLink detects that its to path matches the current URL, it appends the active class to the element. The .nav-item.active rule then applies a blue background so the user always knows which page they are on.
- Save Sidebar.css.
✔️ Awesome, I've got everything!
Great. Make sure both Sidebar.jsx and Sidebar.css are saved.
ⓧ I'd like to double check the full code
Here is the complete Sidebar.jsx:
import { NavLink } from 'react-router'
import './Sidebar.css'
function Sidebar() {
return (
<aside className="sidebar">
<div className="sidebar-header">
<h2>AssetTrack</h2>
</div>
<nav className="sidebar-nav">
<NavLink
to="/"
end
className={({ isActive }) =>
'nav-item' + (isActive ? ' active' : '')
}
>
Dashboard
</NavLink>
<NavLink
to="/assets"
className={({ isActive }) =>
'nav-item' + (isActive ? ' active' : '')
}
>
Assets
</NavLink>
</nav>
</aside>
)
}
export default Sidebar
Your Sidebar.css should contain the container and header styles from the first code block, followed by the nav-item styles from the second code block. Scroll up and confirm both sections are present in that order.
- Create Navbar.jsx in src/components and paste in this code:
import { useLocation } from 'react-router'
import './Navbar.css'
function Navbar() {
// Read the current URL path to pick the right title
const location = useLocation()
// Map each route pathname to a human-readable title
const getTitle = () => {
switch (location.pathname) {
case '/':
return 'Dashboard'
case '/assets':
return 'Assets'
default:
return 'AssetTrack'
}
}
return (
<header className="navbar">
<h1 className="navbar-title">{getTitle()}</h1>
</header>
)
}
export default Navbar
What does useLocation do?
useLocation is a React Router hook that returns the current URL information. The getTitle function reads location.pathname and maps it to a page title. When the user navigates, the Navbar re-renders with the updated title automatically.
- Create Navbar.css in src/components and paste in these styles:
/* Top bar spanning the width above the page content */
.navbar {
height: 60px;
background-color: #ffffff;
border-bottom: 1px solid #dfe6e9;
display: flex;
align-items: center;
padding: 0 24px;
}
/* Page title text style */
.navbar-title {
font-size: 1.25rem;
font-weight: 600;
}
- Save both Navbar.jsx and Navbar.css.
- In the Explorer sidebar, your src/components folder should now contain four files: Navbar.css, Navbar.jsx, Sidebar.css, and Sidebar.jsx.
Don't see all four files?
- Make sure each file is inside src/components/, not directly in src/. If a file ended up in the wrong folder, drag it into components in the Explorer sidebar.
- Check file names for exact spelling and casing. React imports are case-sensitive. Sidebar.jsx and sidebar.jsx are treated as different files.
- Still stuck? Help me check my component file structure.
✔️ Awesome, I've got everything!
Great. Make sure both Navbar files are saved before moving on.
ⓧ I'd like to double check the full code
Here is the complete Navbar.jsx:
import { useLocation } from 'react-router'
import './Navbar.css'
function Navbar() {
const location = useLocation()
const getTitle = () => {
switch (location.pathname) {
case '/':
return 'Dashboard'
case '/assets':
return 'Assets'
default:
return 'AssetTrack'
}
}
return (
<header className="navbar">
<h1 className="navbar-title">{getTitle()}</h1>
</header>
)
}
export default Navbar
And the complete Navbar.css:
.navbar {
height: 60px;
background-color: #ffffff;
border-bottom: 1px solid #dfe6e9;
display: flex;
align-items: center;
padding: 0 24px;
}
.navbar-title {
font-size: 1.25rem;
font-weight: 600;
}
Set up React Router with a layout route
All four component files are ready. Now you need to wire them together. The entry file main.jsx wraps the app with the router so URL-based navigation works. The layout file App.jsx arranges the Sidebar, Navbar, and routed page content on screen.
- Open src/main.jsx in VS Code and replace the entire contents with this code:
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router'
import App from './App'
import './index.css'
// BrowserRouter enables URL-based navigation
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
)
What changed in main.jsx?
BrowserRouter from React Router wraps the entire app so that components like NavLink and useLocation can read and respond to URL changes. Without this wrapper, those components would crash.
- Save main.jsx.
- Check your browser. The Vite starter page with the spinning logos is gone, replaced by a blank gray page. That confirms the router is active and the old demo content is no longer rendered.
- Open src/App.jsx and replace the entire contents with this layout component:
import { Routes, Route } from 'react-router'
import Sidebar from './components/Sidebar'
import Navbar from './components/Navbar'
import Dashboard from './pages/Dashboard'
import Assets from './pages/Assets'
import './App.css'
// Layout shell: sidebar + main area with navbar and routed pages
function App() {
return (
<div className="app-layout">
<Sidebar />
<div className="main-content">
<Navbar />
<div className="page-content">
{/* Routes swaps the visible page based on the URL */}
<Routes>
<Route index element={<Dashboard />} />
<Route path="assets" element={<Assets />} />
</Routes>
</div>
</div>
</div>
)
}
export default App
What does this layout do?
- The outer app-layout div holds the Sidebar and the main content area side by side.
- Sidebar and Navbar render on every page. Only the content inside <Routes> changes when the URL changes.
- The index route renders Dashboard at /. The "assets" route renders Assets at /assets.
- Save App.jsx.
- Check your browser. You should see a dark sidebar on the left with "AssetTrack" at the top, and the word "Dashboard" in the content area. The page content overlaps the sidebar because the layout CSS has not been added yet.
Seeing an error instead of the sidebar?
- Check that all import paths in App.jsx match your folder names exactly. The path ./components/Sidebar requires a folder named components, not component.
- Make sure each component file has export default at the bottom. A missing export causes an import error.
- Confirm main.jsx wraps <App /> inside <BrowserRouter>. Components like NavLink crash if they are rendered outside a router.
- Need more help? Help me debug my React Router setup.
✔️ Awesome, I've got everything!
Great. The overlapping layout is expected. You will fix it in the next substep.
ⓧ I'd like to double check the full code
Here is the complete main.jsx:
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
)
And the complete App.jsx:
import { Routes, Route } from 'react-router'
import Sidebar from './components/Sidebar'
import Navbar from './components/Navbar'
import Dashboard from './pages/Dashboard'
import Assets from './pages/Assets'
import './App.css'
function App() {
return (
<div className="app-layout">
<Sidebar />
<div className="main-content">
<Navbar />
<div className="page-content">
<Routes>
<Route index element={<Dashboard />} />
<Route path="assets" element={<Assets />} />
</Routes>
</div>
</div>
</div>
)
}
export default App
Add CSS for the sidebar layout
The sidebar is rendered with position: fixed, so it stays pinned to the left edge. The main content area does not know about the sidebar yet, which is why it overlaps. A single CSS rule will push the content clear of the sidebar.
- Open src/App.css and replace the entire contents with these layout styles:
/* Horizontal layout: sidebar on the left, content on the right */
.app-layout {
display: flex;
min-height: 100vh;
}
/* Push the content area right to clear the fixed 240px sidebar */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
margin-left: 240px;
}
/* Inner padding and background for the routed page area */
.page-content {
padding: 24px;
background-color: #f5f6fa;
flex: 1;
}
What do these layout styles do?
- app-layout uses flexbox to place the sidebar and content side by side.
- margin-left: 240px on main-content offsets the content area by the exact width of the fixed sidebar. This is the rule that fixes the overlapping layout.
- page-content adds padding around the routed page and a light gray background so the content area is visually distinct from the navbar.
Before you save, predict what you will see. Will the page content still overlap the sidebar, or will it sit cleanly to the right?
- Save App.css.
- Check your browser. The dark sidebar sits on the left with "AssetTrack" and two links. A white navbar stretches across the top showing "Dashboard". The page content area shows the "Dashboard" heading on a light gray background, cleanly to the right of the sidebar.
- Click Assets in the sidebar. The page content changes to show the "Assets" heading. The sidebar and navbar stay fixed.
- Click Dashboard to switch back. The "Dashboard" link now has a blue highlight showing it is the active page.
Content still overlapping the sidebar?
- Check that App.css has margin-left: 240px on the .main-content class. This value must match the sidebar width in Sidebar.css.
- Try a hard refresh with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows) to clear the browser cache.
- Make sure you replaced all of the old Vite CSS in App.css. Any leftover rules from the starter template can conflict.
- Need more help? Help me fix my sidebar layout.
✔️ Awesome, I've got everything!
Great. Make sure App.css is saved and the layout looks clean.
ⓧ I'd like to double check the full code
Here is the complete App.css:
.app-layout {
display: flex;
min-height: 100vh;
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
margin-left: 240px;
}
.page-content {
padding: 24px;
background-color: #f5f6fa;
flex: 1;
}
Your app now has a professional layout shell with persistent navigation. Next up, you will fill the Dashboard with charts and stats that bring the data to life.
Build the Dashboard with Charts
Your app shell is working. The sidebar highlights the active page, the navbar updates its title, and React Router swaps content when you click between pages.
Every page is still an empty placeholder, though. The Dashboard is the first thing a user sees when they load the app. In this step, you will fill it with summary stat cards and two Recharts charts that visualize mock asset data.
In this step, get ready to:
- Build a reusable StatsCard component and render four stat cards on the Dashboard.
- Add a bar chart showing assets by category and a pie chart showing asset status breakdown.
- Style the dashboard layout with CSS Grid so stat cards and charts sit side by side.
Create the mock data file
The Dashboard and Assets pages both need data to display. Instead of hardcoding numbers inside each component, you will create a shared mock data file that all pages import from.
- Create a new file at src/data/mockData.js and paste the dashboard data:
// Summary stats shown on the Dashboard cards
export const statsData = [
{ label: 'Total Assets', value: 8 },
{ label: 'Active', value: 5 },
{ label: 'In Maintenance', value: 2 },
{ label: 'Retired', value: 1 },
]
// Bar chart data: number of assets in each hardware category
export const categoryChartData = [
{ category: 'Laptop', count: 2 },
{ category: 'Monitor', count: 2 },
{ category: 'Server', count: 2 },
{ category: 'Printer', count: 1 },
{ category: 'Phone', count: 1 },
]
// Pie chart data: how many assets are in each lifecycle status
export const statusChartData = [
{ name: 'Active', value: 5 },
{ name: 'In Maintenance', value: 2 },
{ name: 'Retired', value: 1 },
]
- Continue adding the asset records below the code you just pasted, in the same file:
// Full asset records displayed in the Assets table
export const initialAssets = [
{ id: 1, name: 'MacBook Pro M3', category: 'Laptop', status: 'Active', assignedTo: 'Alice', purchaseDate: '2024-01-15' },
{ id: 2, name: 'Dell XPS 15', category: 'Laptop', status: 'Active', assignedTo: 'Bob', purchaseDate: '2024-03-22' },
{ id: 3, name: 'LG UltraWide Monitor', category: 'Monitor', status: 'Active', assignedTo: 'Alice', purchaseDate: '2024-02-10' },
{ id: 4, name: 'Dell U2723QE', category: 'Monitor', status: 'In Maintenance', assignedTo: 'Charlie', purchaseDate: '2023-11-05' },
{ id: 5, name: 'HP ProLiant DL380', category: 'Server', status: 'Active', assignedTo: 'IT Ops', purchaseDate: '2023-06-18' },
{ id: 6, name: 'Dell PowerEdge R750', category: 'Server', status: 'In Maintenance', assignedTo: 'IT Ops', purchaseDate: '2022-09-30' },
{ id: 7, name: 'HP LaserJet Pro', category: 'Printer', status: 'Active', assignedTo: 'Front Desk', purchaseDate: '2024-04-01' },
{ id: 8, name: 'iPhone 15 Pro', category: 'Phone', status: 'Retired', assignedTo: 'Dana', purchaseDate: '2023-01-20' },
]
What does this data represent?
- statsData provides the four headline numbers for the Dashboard stat cards (total count, active, in maintenance, retired).
- categoryChartData and statusChartData feed the bar chart and pie chart you will add to the Dashboard shortly.
- initialAssets holds eight sample IT assets that the Assets page will display in a table. Each object has an id, name, category, status, assignedTo, and purchaseDate.
- Save mockData.js.
- Confirm the file exists by running:
ls src/data
You should see mockData.js listed.
Don't see the file?
Make sure you created the file inside src/data/, not directly in src/ or the project root. If the data folder does not exist yet, create it first, then create mockData.js inside it.
Help me find my mockData file
Create the stat cards
You created src/data/mockData.js in the previous substep. It exports a statsData array with four objects, each containing a label and a value. You need a component that can turn one of those objects into a visual card.
- Create a new file src/components/StatsCard.jsx and add this code:
function StatsCard({ label, value }) {
return (
<div className="stats-card">
{/* Large number displayed prominently */}
<span className="stats-value">{value}</span>
{/* Descriptive label below the number */}
<span className="stats-label">{label}</span>
</div>
)
}
export default StatsCard
What does this component do?
StatsCard is a presentational component. It receives a label (like "Total Assets") and a value (like 8) through props, then renders them inside a card div. It knows nothing about asset data specifically, which makes it reusable anywhere you need a stat display.
- Now replace the placeholder content in src/pages/Dashboard.jsx with this code to import and render the stat cards:
import StatsCard from '../components/StatsCard'
import { statsData } from '../data/mockData'
function Dashboard() {
return (
<div className="dashboard">
{/* Render one StatsCard for each item in the stats array */}
<div className="stats-row">
{statsData.map((stat) => (
<StatsCard key={stat.label} label={stat.label} value={stat.value} />
))}
</div>
</div>
)
}
export default Dashboard
What does this code do?
The statsData.map() call loops through the four stat objects and renders a StatsCard for each one. Each card gets a unique key prop (the label text) so React can track which card is which.
- Save both StatsCard.jsx and Dashboard.jsx.
- In your browser, click Dashboard in the sidebar. You should see four lines of text stacked on the page: the numbers 8, 5, 2, and 1, each with a label below it.
Don't see the stat numbers?
- Make sure you saved both files. Unsaved changes will not trigger a hot reload.
- Check that the import path in Dashboard.jsx is '../components/StatsCard'. A missing ../ prefix means Vite cannot find the file.
- If the page is blank, open your browser console for error messages. Help me fix my Dashboard stat cards.
The numbers are there, but they look like plain unstyled text. Time to give the cards a proper look.
- Create a new file src/components/StatsCard.css and add these styles:
.stats-card {
background-color: #ffffff;
border-radius: 8px;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Large blue number */
.stats-value {
font-size: 2rem;
font-weight: 700;
color: #0984e3;
}
/* Gray label below the number */
.stats-label {
font-size: 0.85rem;
color: #636e72;
margin-top: 4px;
}
What do these styles do?
Each card gets a white background, rounded corners, and a subtle shadow to lift it off the page. The value is styled with a large blue font so the number catches the eye. The label sits below in a smaller gray font.
- Now connect the stylesheet. At the very top of src/components/StatsCard.jsx, add this import:
import './StatsCard.css'
- Save both StatsCard.jsx and StatsCard.css.
- Check your browser. Each stat now appears inside a white card with a large blue number and a small gray label beneath it.
Cards still look unstyled?
- Confirm the import line import './StatsCard.css' is at the very top of StatsCard.jsx, above the function declaration.
- Make sure the CSS file is named StatsCard.css (capital S and C) and sits inside src/components/.
✔️ Awesome, I've got everything!
Great. Double check you have saved both StatsCard files before moving on.
ⓧ I'd like to double check the full code
Here is the complete StatsCard.jsx:
import './StatsCard.css'
function StatsCard({ label, value }) {
return (
<div className="stats-card">
<span className="stats-value">{value}</span>
<span className="stats-label">{label}</span>
</div>
)
}
export default StatsCard
And the complete StatsCard.css:
.stats-card {
background-color: #ffffff;
border-radius: 8px;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.stats-value {
font-size: 2rem;
font-weight: 700;
color: #0984e3;
}
.stats-label {
font-size: 0.85rem;
color: #636e72;
margin-top: 4px;
}
Add a bar chart and a pie chart using Recharts
Stat cards give you the headline numbers. Charts make those numbers visual. Recharts is a React charting library that exposes chart types as composable components, the same pattern you already use for your own UI.
Why Recharts?
Recharts wraps SVG charts in React components. You build a chart the same way you build any UI: by composing smaller pieces (an axis, a tooltip, a bar) inside a parent container. If you can read JSX, you can read a Recharts chart.
You will add two charts to the Dashboard: a bar chart showing how many assets exist in each category (Laptop, Monitor, Server, etc.) and a pie chart showing the status breakdown (Active, In Maintenance, Retired).
- In src/pages/Dashboard.jsx, find the two import lines at the top of the file:
import StatsCard from '../components/StatsCard'
import { statsData } from '../data/mockData'
- Replace those two lines with the following imports to bring in Recharts components, chart data, and a color palette:
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
PieChart,
Pie,
Cell,
} from 'recharts'
import StatsCard from '../components/StatsCard'
import { categoryChartData, statusChartData, statsData } from '../data/mockData'
// One color per status slice: blue for Active, yellow for In Maintenance, red for Retired
const PIE_COLORS = ['#0984e3', '#fdcb6e', '#d63031']
What are all these imports?
- BarChart, Bar, XAxis, YAxis are the building blocks for a bar chart. You compose them like nesting divs.
- PieChart, Pie, and Cell build the pie chart. Each Cell gets its own fill color from PIE_COLORS.
- ResponsiveContainer wraps each chart so it stretches to fill its parent's width, instead of needing a fixed pixel size.
- Next, find the closing </div> of the stats-row div. Add this bar chart code directly below it (still inside the dashboard div):
{/* Charts section */}
<div className="charts-row">
<div className="chart-card">
<h3>Assets by Category</h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={categoryChartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="category" />
<YAxis allowDecimals={false} />
<Tooltip />
<Bar dataKey="count" fill="#0984e3" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</div>
What does this bar chart do?
- BarChart receives categoryChartData (an array of objects like { category: 'Laptop', count: 2 }).
- XAxis reads the category field for labels. Bar reads the count field for bar heights.
- The radius prop rounds the top corners of each bar for a polished look.
- Save Dashboard.jsx and check your browser.
- Below the stat numbers, you should see a bar chart with five blue bars. Hover over a bar to see its tooltip showing the category name and count.
Bar chart not appearing?
- If the page is blank or shows errors, check that the new code sits inside the dashboard div, after the stats-row closing tag. A misplaced closing </div> is the most common cause.
- Confirm the import from '../data/mockData' now includes categoryChartData. A missing export will silently pass undefined to the chart.
- Help me debug my Recharts bar chart.
Now add the pie chart right next to the bar chart.
- In Dashboard.jsx, find the closing </div> of the charts-row div (the last </div> before the dashboard div closes). Add this pie chart code just above that closing tag:
<div className="chart-card">
<h3>Assets by Status</h3>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={statusChartData}
cx="50%"
cy="50%"
outerRadius={100}
dataKey="value"
label={({ name, value }) => `${name}: ${value}`}
>
{/* Assign a unique color to each slice */}
{statusChartData.map((entry, index) => (
<Cell
key={entry.name}
fill={PIE_COLORS[index % PIE_COLORS.length]}
/>
))}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
</div>
What does this pie chart do?
- Pie reads statusChartData and sizes each slice based on the value field (5 for Active, 2 for In Maintenance, 1 for Retired).
- The label prop renders a text label beside each slice showing its name and count.
- Cell components map over the data and assign a color from PIE_COLORS to each slice: blue for Active, yellow for In Maintenance, red for Retired.
- Save Dashboard.jsx and check your browser.
- You should see both charts on the page: the bar chart with five blue bars and the pie chart with three colored slices. They are stacked vertically for now because the layout CSS has not been added yet.
Pie chart not rendering?
- Make sure the pie chart code is inside the charts-row div, not after it. Both chart cards must be siblings inside the same parent.
- Verify that statusChartData is included in the import from '../data/mockData'.
- If you see the bar chart but not the pie chart, count your closing </div> tags. The charts-row div needs exactly one opening and one closing tag wrapping both chart cards.
Style the dashboard layout
Both charts work, but everything is stacked in a single column. A dashboard should show stat cards in a row and charts side by side so you can compare them at a glance.
- Create a new file src/pages/Dashboard.css and add these layout styles:
/* Stack the stat row and charts row vertically */
.dashboard {
display: flex;
flex-direction: column;
gap: 24px;
}
/* Four equal-width stat cards in a row */
.stats-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
/* Two equal-width charts side by side */
.charts-row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
/* White card wrapper for each chart */
.chart-card {
background-color: #ffffff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.chart-card h3 {
margin-bottom: 16px;
font-size: 1rem;
color: #2d3436;
}
What does this CSS do?
- stats-row uses CSS Grid with repeat(4, 1fr) to arrange the four stat cards in a single row of equal widths.
- charts-row uses repeat(2, 1fr) to place the bar chart and pie chart side by side.
- chart-card gives each chart a white card background with rounded corners and a shadow, matching the stat card style.
- Now connect the stylesheet. In src/pages/Dashboard.jsx, find this line:
import { categoryChartData, statusChartData, statsData } from '../data/mockData'
- Add this import directly below it:
import './Dashboard.css'
Before you save, think about what you expect to see. The stat cards are currently stacked vertically. The charts are stacked too. What should the CSS Grid rules change?
- Save both Dashboard.jsx and Dashboard.css.
- Check your browser. You should see four stat cards sitting in a neat row at the top. Below them, the bar chart and pie chart appear side by side in two white card panels.
- Hover over the bar chart bars to confirm the tooltips show category names and counts.
- Hover over the pie chart slices to confirm the tooltips show status names and values.
- Click Assets in the sidebar, then click Dashboard again. The charts should reappear instantly.
Charts still stacked vertically?
- Check that the import './Dashboard.css' line is present in Dashboard.jsx. Without it, none of the grid layout rules apply.
- Confirm the file is named Dashboard.css (capital D) and sits inside src/pages/, not src/components/.
- Help me fix my Dashboard layout.
✔️ Awesome, I've got everything!
Great. Make sure you have saved all files before moving on.
ⓧ I'd like to double check the full code
For Dashboard.jsx, confirm these sections appear in order from top to bottom:
- The Recharts import block (BarChart, Bar, XAxis, etc.).
- The StatsCard import.
- The mockData import with categoryChartData, statusChartData, and statsData.
- The import './Dashboard.css' line.
- The PIE_COLORS constant.
- The Dashboard function with a stats-row div, then a charts-row div containing two chart-card divs (bar chart first, pie chart second).
Here is the complete Dashboard.css:
.dashboard {
display: flex;
flex-direction: column;
gap: 24px;
}
.stats-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.charts-row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.chart-card {
background-color: #ffffff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.chart-card h3 {
margin-bottom: 16px;
font-size: 1rem;
color: #2d3436;
}
Your Dashboard is looking like a real application. The stat cards, bar chart, and pie chart give users an instant overview of the IT asset inventory. Next up, you will build the Assets page with a reusable data table and a form for adding new assets.
Build the Assets Table with Local State
Your Dashboard is alive with charts and statistics. The next challenge is the Assets page, where users need to see and manage individual IT assets instead of just viewing aggregate data.
Right now, clicking Assets in the sidebar shows a bare placeholder. In this step, you will build a reusable table component, populate it with mock data, and add a form for creating new assets. Along the way, you will discover an important limitation of local component state.
In this step, get ready to:
- Build a reusable DataTable component that works with any data shape.
- Display mock IT assets in a styled table on the Assets page.
- Add a modal form for creating new assets with local state.
Create a reusable DataTable component
A good table component knows nothing about the data it displays. It accepts column definitions and row data as props, then renders them. This makes the component reusable for any list of objects, not just assets.
- Create a new file at src/components/DataTable.jsx and paste the following code:
import './DataTable.css'
// Reusable table that works with any columns and data shape
function DataTable({ columns, data, onEdit }) {
return (
<div className="table-container">
<table className="data-table">
<thead>
<tr>
{columns.map((col) => <th key={col.key}>{col.label}</th>)}
{onEdit && <th>Actions</th>}
</tr>
</thead>
<tbody>
{data.length === 0 ? (
<tr>
<td colSpan={columns.length + (onEdit ? 1 : 0)} className="empty-row">No data available</td>
</tr>
) : data.map((row) => (
<tr key={row.id}>
{columns.map((col) => <td key={col.key}>{row[col.key]}</td>)}
{onEdit && (
<td><button className="edit-btn" onClick={() => onEdit(row)}>Edit</button></td>
)}
</tr>
))}
</tbody>
</table>
</div>
)
}
export default DataTable
What does this component do?
The DataTable component receives three props: columns (an array defining header labels and data keys), data (the rows to display), and an optional onEdit callback.
Each cell value is looked up dynamically with row[col.key]. This means the same component can render a table of assets, users, or any other object. You will not need to touch this file when you add new columns later.
- Create a new file at src/components/DataTable.css and paste these table structure styles:
/* Container wraps the table with a card-like appearance */
.table-container {
overflow-x: auto;
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.data-table {
width: 100%;
border-collapse: collapse;
}
/* Shared padding and borders for all cells */
.data-table th,
.data-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid #dfe6e9;
}
.data-table th {
background-color: #f5f6fa;
font-weight: 600;
font-size: 0.85rem;
text-transform: uppercase;
color: #636e72;
}
- Continue adding these interactive styles below the code you just pasted, in the same file:
/* Hover state, empty message, and action button styles */
.data-table tbody tr:hover {
background-color: #f8f9fa;
}
.empty-row {
text-align: center;
color: #b2bec3;
padding: 32px;
}
.edit-btn {
background-color: #0984e3;
color: #ffffff;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
}
.edit-btn:hover {
background-color: #0770c2;
}
- Save DataTable.jsx and DataTable.css.
- Confirm the files exist by running this in your terminal:
ls src/components
You should see DataTable.jsx and DataTable.css alongside your existing Sidebar, Navbar, and StatsCard files.
Don't see the new files?
Make sure you created both files inside src/components/, not in src/ or the project root.
Help me find my DataTable files
Wire up the Assets page with mock data
The DataTable component is ready. Now you need a page that feeds it data. You will use React's useState hook to hold the asset list in local component state, initialized with the mock data you created earlier.
- Replace the contents of src/pages/Assets.jsx with the following code:
import { useState } from 'react'
import { initialAssets } from '../data/mockData'
import DataTable from '../components/DataTable'
import './Assets.css'
// Column definitions tell the DataTable what headers and keys to use
const columns = [
{ key: 'name', label: 'Asset Name' },
{ key: 'category', label: 'Category' },
{ key: 'status', label: 'Status' },
{ key: 'assignedTo', label: 'Assigned To' },
{ key: 'purchaseDate', label: 'Purchase Date' },
]
function Assets() {
// Local state holds the asset list, initialized with mock data
const [assets, setAssets] = useState(initialAssets)
const [showModal, setShowModal] = useState(false)
const handleAdd = () => { setShowModal(true) }
return (
<div className="assets-page">
<div className="assets-header">
<h2>All Assets</h2>
<button className="btn-add" onClick={handleAdd}>+ Add Asset</button>
</div>
<DataTable columns={columns} data={assets} />
</div>
)
}
export default Assets
What does this code do?
- columns is an array of objects that define what headers the table shows and which key to pull from each asset object.
- useState(initialAssets) copies the mock data into local component state. The table reads from this state, and new assets will be added to it.
- The + Add Asset button sets showModal to true. You will connect a modal form to this state in the next substep.
- Create a new file at src/pages/Assets.css and paste these styles:
/* Page layout, header row, and add button styles */
.assets-page {
display: flex;
flex-direction: column;
gap: 16px;
}
.assets-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.assets-header h2 {
font-size: 1.25rem;
}
.btn-add {
background-color: #00b894;
color: #ffffff;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 0.95rem;
font-weight: 500;
}
.btn-add:hover {
background-color: #00a381;
}
- Save both Assets.jsx and Assets.css.
- Switch to your browser at http://localhost:5173 and click Assets in the sidebar.
You should see a styled table with eight rows of mock asset data, column headers for Asset Name, Category, Status, Assigned To, and Purchase Date, plus a green + Add Asset button in the top-right corner.
See a blank page or an error?
- Check that DataTable.jsx is in src/components/ and that the import path in Assets.jsx is ../components/DataTable.
- Make sure Assets.css is in src/pages/, not in src/components/.
- Look at your terminal for error messages. A typo in a file name or import path will show up there.
Help me fix my Assets table
✔️ Awesome, I've got everything!
Make sure you have saved both Assets.jsx and Assets.css.
ⓧ I'd like to double check the full code
Scroll up and confirm each of these code blocks is present and saved in the correct file:
- src/components/DataTable.jsx contains the reusable table component with columns, data, and onEdit props.
- src/components/DataTable.css contains the table container styles (chunk 1) and hover/button styles (chunk 2).
- src/pages/Assets.jsx imports initialAssets and DataTable, defines columns, and renders the table with a + Add Asset button.
- src/pages/Assets.css contains the page layout and button styles.
Add a modal form for adding assets
The table displays existing assets, but users need a way to add new ones. You will build a modal form that pops up over the page, collects asset details, and adds the new entry to local state.
- Create a new file at src/components/AssetModal.jsx and paste the component logic:
import { useState, useEffect } from 'react'
import './AssetModal.css'
function AssetModal({ asset, onSave, onClose }) {
// Form fields with sensible defaults for new assets
const [formData, setFormData] = useState({
name: '', category: 'Laptop', status: 'Active',
assignedTo: '', purchaseDate: '',
})
// When editing, pre-fill the form with the existing asset data
useEffect(() => {
if (asset) {
setFormData({
name: asset.name, category: asset.category,
status: asset.status, assignedTo: asset.assignedTo,
purchaseDate: asset.purchaseDate,
})
}
}, [asset])
// Update the matching form field when any input changes
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value })
}
// Validate name is not empty, then pass data to parent
const handleSubmit = (e) => {
e.preventDefault()
if (!formData.name.trim()) return
onSave({ ...formData, id: asset ? asset.id : Date.now() })
}
- Still in AssetModal.jsx, add the form's return statement directly below the handleSubmit function:
// Clicking the overlay background closes the modal
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<h2>{asset ? 'Edit Asset' : 'Add Asset'}</h2>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="name">Asset Name</label>
<input id="name" name="name" type="text" value={formData.name} onChange={handleChange} required />
</div>
<div className="form-group">
<label htmlFor="category">Category</label>
<select id="category" name="category" value={formData.category} onChange={handleChange}>
<option value="Laptop">Laptop</option>
<option value="Monitor">Monitor</option>
<option value="Server">Server</option>
<option value="Printer">Printer</option>
<option value="Phone">Phone</option>
</select>
</div>
<div className="form-group">
<label htmlFor="status">Status</label>
<select id="status" name="status" value={formData.status} onChange={handleChange}>
<option value="Active">Active</option>
<option value="In Maintenance">In Maintenance</option>
<option value="Retired">Retired</option>
</select>
</div>
- Add the remaining form fields and the action buttons directly below the status form group:
<div className="form-group">
<label htmlFor="assignedTo">Assigned To</label>
<input id="assignedTo" name="assignedTo" type="text" value={formData.assignedTo} onChange={handleChange} />
</div>
<div className="form-group">
<label htmlFor="purchaseDate">Purchase Date</label>
<input id="purchaseDate" name="purchaseDate" type="date" value={formData.purchaseDate} onChange={handleChange} />
</div>
<div className="modal-actions">
<button type="button" className="btn-cancel" onClick={onClose}>Cancel</button>
<button type="submit" className="btn-save">{asset ? 'Update' : 'Add'} Asset</button>
</div>
</form>
</div>
</div>
)
}
export default AssetModal
What does this modal do?
- formData state holds the value of every form field. Each input's name attribute matches a key in formData, so one handleChange function handles all five fields.
- The useEffect pre-fills the form when an existing asset is passed in. You will use this for editing in the next step.
- Clicking the dark overlay calls onClose. The e.stopPropagation() on the inner content prevents the overlay click from firing when you interact with the form.
- Create a new file at src/components/AssetModal.css and paste the overlay and content styles:
/* Semi-transparent overlay covers the entire viewport */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.modal-content {
background-color: #ffffff;
border-radius: 8px;
padding: 24px;
width: 480px;
max-width: 90vw;
max-height: 90vh;
overflow-y: auto;
}
.modal-content h2 {
margin-bottom: 20px;
}
.form-group {
margin-bottom: 16px;
}
- Add the form input and label styles below:
.form-group label {
display: block;
margin-bottom: 4px;
font-weight: 500;
font-size: 0.9rem;
color: #636e72;
}
.form-group input,
.form-group select {
width: 100%;
padding: 8px 12px;
border: 1px solid #dfe6e9;
border-radius: 4px;
font-size: 0.95rem;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #0984e3;
}
- Add the action button styles below:
/* Button row pinned to the bottom of the form */
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 20px;
}
.btn-cancel {
padding: 8px 16px;
border: 1px solid #dfe6e9;
background-color: #ffffff;
border-radius: 4px;
cursor: pointer;
}
.btn-save {
padding: 8px 16px;
background-color: #0984e3;
color: #ffffff;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-save:hover {
background-color: #0770c2;
}
- Save both AssetModal.jsx and AssetModal.css.
- Confirm the new files exist by running:
ls src/components
You should see AssetModal.jsx and AssetModal.css alongside the DataTable, Sidebar, Navbar, and StatsCard files.
Now you need to connect the modal to the Assets page. This requires three small additions to Assets.jsx.
- In src/pages/Assets.jsx, find this import line:
import DataTable from '../components/DataTable'
- Add this import directly below it:
import AssetModal from '../components/AssetModal'
- Find the handleAdd function. Add these two handler functions below it:
// Add the new asset to local state and close the modal
const handleSave = (assetData) => {
setAssets([...assets, assetData])
setShowModal(false)
}
// Close the modal without saving
const handleClose = () => {
setShowModal(false)
}
- Find the <DataTable columns={columns} data={assets} /> line. Add the modal rendering directly below it, before the closing </div>:
{showModal && (
<AssetModal
asset={null}
onSave={handleSave}
onClose={handleClose}
/>
)}
What's happening here?
handleSave spreads the existing assets into a new array and appends the new asset. This triggers a re-render, and the DataTable immediately shows the new row.
The {showModal && ...} pattern is a conditional render. The modal only mounts in the DOM when showModal is true. Passing asset={null} tells the modal this is a create action, not an edit.
- Save Assets.jsx.
- Switch to your browser and click Assets in the sidebar.
- Click the + Add Asset button.
A modal should appear with fields for Asset Name, Category, Status, Assigned To, and Purchase Date.
- Type Test Laptop in the Asset Name field.
- Leave the other fields at their defaults.
- Click Add Asset.
The modal closes and a ninth row appears at the bottom of the table with your new asset. The add feature works.
Modal not appearing or form not saving?
- If nothing happens when you click the button, check that the import AssetModal line is present and the file path is ../components/AssetModal.
- If the modal appears but submitting does nothing, confirm that handleSave is passed as the onSave prop to AssetModal.
Help me debug my asset modal
Now for the real test. Before you try this, think for a moment: what do you expect will happen to your new "Test Laptop" asset if you click Dashboard in the sidebar, then click Assets again?
- Click Dashboard in the sidebar.
- Click Assets in the sidebar to return.
Your "Test Laptop" is gone. The table is back to the original eight mock assets.
Why did the new asset disappear?
When you navigated to Dashboard, React Router unmounted the Assets component. All of its local state was destroyed. When you navigated back, React Router mounted a fresh Assets component, and useState(initialAssets) ran again with the original mock data.
This is the fundamental limitation of useState for data that needs to survive across page navigation. The state lives inside the component, so it dies with the component.
✔️ Awesome, I've got everything!
Make sure all six files are saved: DataTable.jsx, DataTable.css, Assets.jsx, Assets.css, AssetModal.jsx, and AssetModal.css.
ⓧ I'd like to double check the full code
Scroll up and confirm each of these code blocks is present and saved in the correct file:
- src/components/DataTable.jsx has the reusable table component with columns, data, and onEdit props.
- src/components/DataTable.css has the table container styles (chunk 1) and hover/button styles (chunk 2).
- src/components/AssetModal.jsx has the logic chunk (state, useEffect, handlers) then the JSX return chunk (form overlay, form fields, action buttons).
- src/components/AssetModal.css has the overlay/content styles (chunk 1), form input styles (chunk 2), and button styles (chunk 3).
- src/pages/Assets.jsx imports DataTable and AssetModal, defines handleAdd, handleSave, and handleClose, and renders the DataTable with the conditional modal.
- src/pages/Assets.css has the page layout and green add-button styles.
You have a working table and form, but local state is clearly not enough. Next up, you will fix this data loss by moving the asset list into a Redux Toolkit global store that lives outside any single component.
Fix State Loss with Redux Toolkit
In the last step, you hit a real limitation. Assets you added vanished the instant you navigated to the Dashboard and back.
That happened because useState resets every time a component unmounts. Redux Toolkit solves this by keeping data in a global store that lives outside any single component. In this step, you will connect the Assets page to that store and add edit functionality.
In this step, get ready to:
- Connect the Assets page to the global Redux store so data persists across navigation.
- Add edit functionality to update existing assets through the modal form.
- Verify that adding and editing assets survives page navigation.
Install Redux Toolkit and create the store
- Install Redux Toolkit and React Redux by running this command:
npm install @reduxjs/toolkit react-redux
- Create a src/store folder for the Redux files by running:
mkdir src/store
npm install failing?
- Make sure your terminal is inside the it-asset-dashboard project folder. Run ls and confirm you see package.json in the output.
- If you see a permissions error, try sudo npm install @reduxjs/toolkit react-redux on macOS/Linux.
Help me fix my npm install
- Create a new file at src/store/assetsSlice.js and paste the following code:
import { createSlice } from '@reduxjs/toolkit'
import { initialAssets } from '../data/mockData'
// Slice that holds the asset list and defines how it can change
const assetsSlice = createSlice({
name: 'assets',
initialState: {
items: initialAssets,
},
reducers: {
// Append a new asset to the list
addAsset: (state, action) => {
state.items.push(action.payload)
},
// Replace the matching asset by id
updateAsset: (state, action) => {
const index = state.items.findIndex((a) => a.id === action.payload.id)
if (index !== -1) {
state.items[index] = action.payload
}
},
// Remove the asset with the given id
deleteAsset: (state, action) => {
state.items = state.items.filter((a) => a.id !== action.payload)
},
},
})
export const { addAsset, updateAsset, deleteAsset } = assetsSlice.actions
export default assetsSlice.reducer
What does this code do?
- createSlice defines a slice of Redux state. The name (assets) is used as a prefix for action types.
- initialState seeds the store with the same mock data from mockData.js. This replaces the old useState(initialAssets) call in Assets.jsx.
- The three reducers (addAsset, updateAsset, deleteAsset) use Immer under the hood, so you can write mutations like state.items.push() and Redux handles immutability for you.
- Create a new file at src/store/store.js and paste this code:
import { configureStore } from '@reduxjs/toolkit'
import assetsReducer from './assetsSlice'
// Create the global store with the assets slice registered
const store = configureStore({
reducer: {
assets: assetsReducer,
},
})
export default store
What does store.js do?
configureStore creates the global Redux store and registers the assets reducer under the key assets. This means any component can read the asset list from state.assets.items. It also auto-configures Redux DevTools so you can inspect state changes in the browser.
- Save both assetsSlice.js and store.js.
- Confirm the files exist by running:
ls src/store
You should see assetsSlice.js and store.js listed.
Don't see the store files?
Make sure you created both files inside src/store/, not in src/ or the project root. If the store folder doesn't exist, run mkdir src/store first.
Help me find my store files
Connect the store to your app
The store exists, but your app doesn't know about it yet. You need to wrap your entire app with the Redux Provider component in main.jsx so every component can access the store.
- Open src/main.jsx in VS Code.
- Select all the existing code and replace it with the following:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router'
import { Provider } from 'react-redux'
import store from './store/store'
import App from './App.jsx'
import './index.css'
createRoot(document.getElementById('root')).render(
<StrictMode>
{/* Provider makes the Redux store available to all components */}
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
</StrictMode>
)
What changed in main.jsx?
- Two new imports: Provider from react-redux and the store you just created.
- Provider wraps BrowserRouter so every component inside the router can use useSelector and useDispatch to read from and write to the store.
- Because the store lives outside the React component tree, its data survives when components unmount during navigation.
- Save main.jsx.
- Switch to your browser at http://localhost:5173.
The app should load normally with no visible changes. The Redux store is now running behind the scenes, ready for the Assets page to connect to it.
App crashing after adding Provider?
- Check that the import path is ./store/store (two levels: folder name then file name). A common mistake is ./store which would look for an index.js that doesn't exist.
- Make sure Provider wraps around BrowserRouter, not inside it. The nesting order matters.
Why is my app crashing after I added the Redux Provider?
Refactor the Assets page to use the Redux store
Right now, Assets.jsx stores assets in local useState. You need to replace that with Redux hooks that read from and write to the global store.
- Open src/pages/Assets.jsx in VS Code.
- Select all the existing code and delete it.
- Replace it with the following code, starting with the imports and component setup:
import { useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { addAsset, updateAsset } from '../store/assetsSlice'
import DataTable from '../components/DataTable'
import AssetModal from '../components/AssetModal'
import './Assets.css'
// Column headers for the assets data table
const columns = [
{ key: 'name', label: 'Asset Name' },
{ key: 'category', label: 'Category' },
{ key: 'status', label: 'Status' },
{ key: 'assignedTo', label: 'Assigned To' },
{ key: 'purchaseDate', label: 'Purchase Date' },
]
function Assets() {
// Read assets from the Redux store instead of local state
const assets = useSelector((state) => state.assets.items)
const dispatch = useDispatch()
const [showModal, setShowModal] = useState(false)
// Track which asset is being edited (null = adding new)
const [editingAsset, setEditingAsset] = useState(null)
What changed from the previous version?
- useSelector reads state.assets.items from the Redux store. This replaces the old useState(initialAssets) call that reset every time the component unmounted.
- useDispatch gives you a function to send actions (like addAsset or updateAsset) to the store.
- editingAsset is new. When it is null, the modal is in "add" mode. When it holds an asset object, the modal is in "edit" mode.
- Continue below that code with the handler functions:
const handleAdd = () => {
setEditingAsset(null)
setShowModal(true)
}
const handleEdit = (asset) => {
// Store the asset being edited so the modal pre-fills its fields
setEditingAsset(asset)
setShowModal(true)
}
// Dispatch the correct Redux action based on add vs edit mode
const handleSave = (assetData) => {
if (editingAsset) {
dispatch(updateAsset(assetData))
} else {
dispatch(addAsset(assetData))
}
setShowModal(false)
setEditingAsset(null)
}
const handleClose = () => {
setShowModal(false)
setEditingAsset(null)
}
What do these handlers do?
- handleSave checks editingAsset. If it exists, the learner is editing an existing asset, so it dispatches updateAsset. Otherwise it dispatches addAsset.
- handleEdit stores the clicked row's asset object. The modal uses this to pre-fill its form fields.
- Both handleSave and handleClose reset editingAsset to null so the next modal open starts fresh.
- Finish the file by adding the JSX return and export below the handlers:
return (
<div className="assets-page">
<div className="assets-header">
<h2>All Assets</h2>
<button className="btn-add" onClick={handleAdd}>
+ Add Asset
</button>
</div>
{/* Pass onEdit to enable the Edit button on each row */}
<DataTable columns={columns} data={assets} onEdit={handleEdit} />
{showModal && (
<AssetModal
asset={editingAsset}
onSave={handleSave}
onClose={handleClose}
/>
)}
</div>
)
}
export default Assets
What changed in the JSX?
- onEdit={handleEdit} is new. This prop tells DataTable to render an Edit button on each row. You already built DataTable to support this in the previous step.
- asset={editingAsset} replaces the old asset={null}. Now the modal receives the asset object being edited and pre-fills its form fields automatically.
- Save src/pages/Assets.jsx.
Before you test, think about what will happen when you add a new asset and navigate to Dashboard and back. Will the asset survive this time?
- Switch to your browser at http://localhost:5173.
- Click Assets in the sidebar.
- Click + Add Asset.
- Enter Test Monitor as the asset name and click Add Asset.
- Click Dashboard in the sidebar.
- Click Assets again.
Your "Test Monitor" asset is still in the table. The Redux store kept it safe while the Assets component unmounted and remounted.
Asset still disappearing after navigation?
- Make sure you saved Assets.jsx. Unsaved changes mean the old useState version is still running.
- Check that your imports include useSelector from react-redux and addAsset from ../store/assetsSlice. A missing import causes the component to crash silently.
- Confirm that handleSave calls dispatch(addAsset(assetData)) in the else branch, not setAssets.
Why is my added asset disappearing after I navigate away?
Test the edit functionality
The code you just pasted also includes edit support. Each table row now has an Edit button because you passed the onEdit prop to DataTable.
- Click the Edit button on any row in the assets table.
- The modal opens with that asset's data pre-filled in every field.
- Change the Status dropdown to a different value (for example, change Active to In Maintenance).
- Click Update Asset.
The table updates immediately with your change. Navigate to the Dashboard and back to confirm the edit persisted.
Why Redux Toolkit over local state?
With useState, each component owns its data. When React Router swaps components during navigation, the outgoing component unmounts and its state is destroyed.
Redux Toolkit moves that data into a store that exists at the application level. Components read from the store using useSelector and update it using dispatch. The store stays alive regardless of which components are mounted.
🙋♀️ Edit button not showing up?
- Check that your DataTable line includes onEdit={handleEdit}. Without this prop, DataTable hides the Actions column entirely.
- If the modal opens but fields are empty when editing, confirm that AssetModal receives asset={editingAsset} instead of asset={null}.
Why isn't the Edit button appearing in my assets table?
✔️ Awesome, I've got everything!
Double check that you saved src/pages/Assets.jsx.
ⓧ I'd like to double check the full code
The complete Assets.jsx is built from the three code blocks above. Scroll up and confirm each section is present in order:
- Block 1: Imports, columns array, function declaration with useSelector, useDispatch, and state hooks.
- Block 2: Four handler functions (handleAdd, handleEdit, handleSave, handleClose).
- Block 3: The return JSX with DataTable receiving onEdit and AssetModal receiving editingAsset, plus export default Assets.
Your dashboard app now has full add and edit functionality backed by a global Redux store. Every change survives page navigation. Next up, take on the Secret Mission and add search and filtering to your Assets table.
Secret mission
Add Search and Filter to the Assets Table
Your Assets table works great with 8 items, but real IT departments manage hundreds. Can you add a search bar and status filter that narrow down the table in real time, without touching the Redux store?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- The asset-manager project folder on your Desktop (contains all source files and node_modules).
- The Vite dev server running at http://localhost:5173.
Keep everything running
No action needed. Choose this if you are still actively building or want to keep experimenting with the app.
- Your dev server will keep running at http://localhost:5173 until you close the terminal or press Ctrl+C.
- The asset-manager folder stays on your Desktop for future use.
Pause - I'll come back to this later
Shut down the running dev server to free up your terminal and port, but keep all your project files for later.
- In the terminal where the dev server is running, press Ctrl+C to stop the server.
- When you are ready to resume, open a terminal in the asset-manager folder and run npm run dev to start the server again.
Delete - I don't want to use this again
Remove all project files and start fresh. This cannot be undone.
- In the terminal where the dev server is running, press Ctrl+C to stop the server.
- Delete the project folder by running this command on macOS/Linux:
cd ~/Desktop
rm -rf asset-manager
- On Windows, run this command instead:
cd ~\Desktop
rmdir /s /q asset-manager
- Confirm the folder is gone by running ls (macOS/Linux) or dir (Windows). You should no longer see asset-manager in the output.
Nice Work!
Nice Work!
You just built a fully functional IT Asset Management Dashboard from scratch. You can navigate between pages, visualize data with charts, and create or edit assets that persist across the entire app.
You've learned how to:
- Build a multi-page React application with a persistent Sidebar and Navbar layout using React Router for client-side routing between Dashboard and Assets pages.
- Create interactive bar and pie charts with Recharts and a reusable DataTable component that accepts any data shape through props.
- Solve state loss across navigation by moving from local useState to a global Redux Toolkit store with full add and edit asset functionality through a modal form.
- Secret Mission: Added real-time search and status filtering to the Assets table using controlled components and derived state that filters the Redux store without modifying it.
Ready to quiz yourself?