Extract card component
You have 6 identical card divs in the features page. Each one has the same structure:
<div style={{ padding: '2rem', border: '1px solid #e5e7eb', borderRadius: '0.5rem' }}>
<h3>Title</h3>
<p>Content</p>
</div>This has been copy-pasted 6 times. Change the border color? Update 6 places. Add this card to another page? Copy-paste again.
Extract it once into packages/ui, and all instances stay in sync automatically.
Outcome
Extract the Card component to a shared package and experience instant workspace synchronization across all apps.
Fast track
- Create
packages/ui/src/card.tsxcomponent with title and children props - Add
Cardto package exports (package.json + index.ts) - Add
@geniusgarage/uidependency to web app + configure transpilation - Replace inline divs with
<Card>components in features page - Test instant updates by changing
Cardstyles in dev mode
Hands-on exercise 2.3
Extract the 6 duplicate card divs from the features page into a reusable Card component in the shared UI package.
Requirements:
- Create
Cardcomponent inpackages/ui/src/card.tsxwith optional title and children props - Export
Cardfrompackages/ui(both package.json exports field and index.ts) - Add
@geniusgarage/uias a workspace dependency inapps/web/package.json - Replace all 6 inline card divs with
<Card>components - Verify instant hot-reload updates when modifying the
Cardcomponent
Implementation hints:
- Use
title?: stringfor optional title prop (renders h3 only if provided) - Use
workspace:*protocol in package.json to link local package (not npm) - Next.js 15+ with Turbopack automatically handles local packages - no additional config needed
- Test the monorepo magic: change Card border color and watch all 6 cards update instantly
- Keep the dev server running during changes to see hot module replacement work
Files to create/modify:
packages/ui/src/card.tsx(new file)packages/ui/package.json(add exports field)packages/ui/src/index.ts(export Card)apps/web/package.json(add dependency)apps/web/app/features/page.tsx(use Card component)
Create the card component
Create packages/ui/src/card.tsx with the component code:
export interface CardProps {
title?: string
children: React.ReactNode
}
export function Card({ title, children }: CardProps) {
return (
<div style={{
padding: '2rem',
border: '1px solid #e5e7eb',
borderRadius: '0.5rem',
}}>
{title && (
<h3 style={{
fontSize: '1.25rem',
fontWeight: 'bold',
marginTop: 0,
marginBottom: '0.5rem'
}}>
{title}
</h3>
)}
<div style={{ color: '#666' }}>
{children}
</div>
</div>
)
}This extracts the structure you've been copying. The title prop is optional - if provided, it renders an h3.
Add card to package exports
Update packages/ui/package.json to export the Card:
{
"name": "@geniusgarage/ui",
"version": "1.0.0",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
"./card": "./src/card.tsx"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"devDependencies": {
"@types/react": "^19",
"@types/react-dom": "^19",
"typescript": "^5"
}
}The "./card": "./src/card.tsx" export means apps can import with:
import { Card } from '@geniusgarage/ui/card'Also export from the index file:
export { Card } from './card'Add UI package to web app
The web app needs to declare its dependency on the UI package. Open apps/web/package.json:
{
"name": "@geniusgarage/web",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@geniusgarage/ui": "workspace:*",
"next": "^16.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/node": "^22",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "^16.0.0",
"typescript": "^5"
}
}The workspace:* protocol tells pnpm to link to the local packages/ui instead of looking for it on npm.
What workspace:* means:
workspace:= use local package from this monorepo*= use whatever version the package declares (don't version-lock)- Result: instant updates without publishing to npm
If using npm instead of pnpm, use "*" instead of "workspace:*":
"dependencies": {
"@geniusgarage/ui": "*"
}npm workspaces don't support the workspace: protocol, but * achieves the same local linking behavior.
Install to create the symlink. A symlink (symbolic link) is a file that points to another file or directory, like a shortcut. In this case, it points to the packages/ui directory.
pnpm installYou'll see output confirming the workspace link:
Packages: +1
+
Progress: resolved 1, reused 0, downloaded 0, added 1, done
Next.js automatically handles local packages
With Next.js 15+ and Turbopack (now the default), local monorepo packages work automatically - no configuration needed! Next.js detects workspace packages and compiles them on the fly.
Previous Next.js versions required transpilePackages: ['@geniusgarage/ui'] in next.config.js. With modern Next.js and Turbopack, this is no longer necessary for local packages.
If you're using an older Next.js version (< 15) without Turbopack, you may need to add:
module.exports = {
transpilePackages: ['@geniusgarage/ui'],
}Use card in features page
Now replace the inline cards with the shared Card component. Open apps/web/app/features/page.tsx:
import Link from 'next/link'
import { Card } from '@geniusgarage/ui/card'
export default function Features() {
return (
<main style={{ padding: '4rem 2rem', fontFamily: 'system-ui', maxWidth: '1200px', margin: '0 auto' }}>
<nav style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4rem' }}>
<Link href="/" style={{ fontSize: '1.5rem', fontWeight: 'bold', textDecoration: 'none', color: '#000' }}>
🧠 GeniusGarage
</Link>
<div style={{ display: 'flex', gap: '2rem' }}>
<Link href="/features" style={{ textDecoration: 'none', color: '#000', fontWeight: 'bold' }}>Features</Link>
</div>
</nav>
<h1 style={{ fontSize: '2.5rem', marginBottom: '1rem', textAlign: 'center' }}>Features</h1>
<p style={{ fontSize: '1.2rem', color: '#666', marginBottom: '3rem', textAlign: 'center' }}>
Everything you need to manage your code snippets
</p>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '2rem' }}>
<Card title="⚡ Fast Search">
Find your snippets instantly with powerful full-text search and filtering by tags
</Card>
<Card title="📁 Organized">
Keep your code organized with tags, folders, and collections
</Card>
<Card title="🔗 Shareable">
Share snippets with your team or make them public for the community
</Card>
<Card title="🎨 Syntax Highlighting">
Beautiful syntax highlighting for 100+ programming languages
</Card>
<Card title="📋 One-Click Copy">
Copy snippets to your clipboard with a single click
</Card>
<Card title="🔐 Private & Secure">
Your private snippets stay private with enterprise-grade security
</Card>
</div>
</main>
)
}Look how clean this is! From 6 duplicate <div> blocks to 6 <Card> components.
Try it
1. Start the dev server
pnpm devYou should see:
>>> @geniusgarage/web:dev: ready started server on 0.0.0.0:3000
>>> @geniusgarage/web:dev: ▲ Next.js 16.0.0
>>> @geniusgarage/web:dev: - Local: http://localhost:3000
>>> @geniusgarage/web:dev: ✓ Compiled /features in 247ms
2. Open the features page
Navigate to http://localhost:3000/features
You should see:
- 6 cards in a responsive grid
- Each card has an emoji + title (⚡ Fast Search, 📁 Organized, etc.)
- Gray borders around each card
The page looks identical to before, but now uses the shared Card component from packages/ui.
Experience instant workspace updates
3. Test the monorepo magic
With the dev server still running, change the Card component's border style:
export interface CardProps {
title?: string
children: React.ReactNode
}
export function Card({ title, children }: CardProps) {
return (
<div style={{
padding: '2rem',
border: '2px solid #3b82f6', // Changed: Blue border instead of gray
borderRadius: '0.5rem',
}}>
{title && (
<h3 style={{
fontSize: '1.25rem',
fontWeight: 'bold',
marginTop: 0,
marginBottom: '0.5rem'
}}>
{title}
</h3>
)}
<div style={{ color: '#666' }}>
{children}
</div>
</div>
)
}Save the file. Watch your browser - the dev server hot-reloads and all 6 cards instantly update with the new blue border. No manual refresh needed.
This is workspace dependency magic: Change the component once in packages/ui, and all apps using it update immediately. No npm publishing, no version bumping, no waiting. Just instant synchronization.
4. Change it back
Before continuing, revert to the original gray border:
export function Card({ title, children }: CardProps) {
return (
<div style={{
padding: '2rem',
border: '1px solid #e5e7eb', // Back to gray
borderRadius: '0.5rem',
}}>
{/* Keep all other code (title rendering, children) the same */}
</div>
)
}You just experienced the core monorepo benefit:
- Extract component to shared package
- Import with
workspace:* - Change component → instant updates everywhere
No publishing, no registry, no waiting. Just instant synchronization.
What you built
You created your first shared component:
Before:
// 6 duplicate divs with inline styles
<div style={{ padding: '2rem', border: '1px solid #e5e7eb', ... }}>
<h3>⚡ Fast Search</h3>
<p>Find your snippets...</p>
</div>After:
// 6 clean component calls
<Card title="⚡ Fast Search">
Find your snippets...
</Card>Change the border color, add animations, update spacing - do it once in packages/ui/src/card.tsx and all 6 cards update automatically.
Troubleshooting
If you get a TypeScript error about react/jsx-runtime, make sure your packages/ui/package.json includes the devDependencies section with React types:
"devDependencies": {
"@types/react": "^19",
"@types/react-dom": "^19",
"typescript": "^5"
}Then run pnpm install to install the types.
Commit
Save your work:
git add .
git commit -m "feat(ui): extract Card component to shared package"Done-when
Verify your implementation:
- Card component exists at
packages/ui/src/card.tsxwith title and children props - Card is exported from
packages/ui/package.jsonexports field - Card is exported from
packages/ui/src/index.ts - Web app has
@geniusgarage/ui: "workspace:*"dependency - Features page displays 6 cards using
<Card>components - Changing Card border color updates all 6 cards instantly in dev mode
- Page loads at http://localhost:3000/features with responsive grid
What's Next
Next lesson: Add More Components with v0 - You'll use Vercel's v0 to generate a pricing card component, extract Button from the home page, add a lint task to the UI package, and experience Turborepo orchestrating tasks across packages with caching.
Was this helpful?