Your First Component
React applications are built from components — reusable pieces of UI that manage their own state and render based on props. Let's build a simple counter component to understand the basics.
'use client';
import { useState } from 'react';
interface CounterProps {
initialCount?: number;
label?: string;
}
export default function Counter({ initialCount = 0, label = "Count" }: CounterProps) {
const [count, setCount] = useState(initialCount);
return (
<div className="flex flex-col items-center gap-4 p-6 rounded-xl border border-gray-200">
<h2 className="text-lg font-semibold text-gray-700">{label}</h2>
<span className="text-5xl font-bold text-blue-600">{count}</span>
<div className="flex gap-3">
<button
onClick={() => setCount(c => c - 1)}
className="px-4 py-2 bg-red-100 text-red-600 rounded-lg hover:bg-red-200 transition-colors"
>
−
</button>
<button
onClick={() => setCount(initialCount)}
className="px-4 py-2 bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200 transition-colors"
>
Reset
</button>
<button
onClick={() => setCount(c => c + 1)}
className="px-4 py-2 bg-green-100 text-green-600 rounded-lg hover:bg-green-200 transition-colors"
>
+
</button>
</div>
</div>
);
}
Using the Component
Now that we have our Counter component, let's use it in a page. Components are composable — you can use them multiple times with different props.
import Counter from '@/components/Counter';
export default function HomePage() {
return (
<main className="min-h-screen flex flex-col items-center justify-center gap-8 p-8">
<h1 className="text-4xl font-bold">My First React App</h1>
<div className="flex flex-wrap gap-6 justify-center">
<Counter label="Lives" initialCount={3} />
<Counter label="Score" initialCount={0} />
<Counter label="Level" initialCount={1} />
</div>
</main>
);
}
Adding Side Effects
The useEffect hook lets you perform side effects in your components — like fetching data, updating the document title, or setting up subscriptions.
'use client';
import { useState, useEffect } from 'react';
export default function TitleUpdater() {
const [count, setCount] = useState(0);
// Update document title whenever count changes
useEffect(() => {
document.title = `Count: ${count} | My App`;
// Cleanup function runs before next effect or unmount
return () => {
document.title = 'My App';
};
}, [count]); // Dependency array: re-run when count changes
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
}
The dependency array in useEffect controls when the effect re-runs. An empty array [] means the effect only runs once on mount.
Fetching Data
In Next.js App Router, you can fetch data directly in Server Components without useEffect. This is more efficient and secure.
// This is a Server Component — runs on the server
// No 'use client' needed, no useEffect needed
interface User {
id: number;
name: string;
email: string;
}
async function getUsers(): Promise<User[]> {
const res = await fetch('https://jsonplaceholder.typicode.com/users', {
next: { revalidate: 3600 } // Cache for 1 hour
});
if (!res.ok) throw new Error('Failed to fetch users');
return res.json();
}
export default async function UsersPage() {
const users = await getUsers();
return (
<div className="container mx-auto p-8">
<h1 className="text-3xl font-bold mb-6">Users</h1>
<ul className="space-y-3">
{users.map(user => (
<li key={user.id} className="p-4 bg-white rounded-lg shadow">
<p className="font-semibold">{user.name}</p>
<p className="text-gray-500 text-sm">{user.email}</p>
</li>
))}
</ul>
</div>
);
}