Czas nauczyć się budować reużywalne komponenty - takie, które możesz używać w wielu miejscach z różnymi danymi.
Props (properties) to dane przekazywane do komponentu od rodzica (parent).
1function PirateCard() {
2 return (
3 <div className="border p-4 rounded-lg">
4 <h2 className="text-xl font-bold">Jack Sparrow</h2>
5 <p>Level: 10</p>
6 <p>Ship: Black Pearl</p>
7 </div>
8 );
9}Problem: Możemy pokazać tylko Jacka Sparrowa. Co z innymi piratami?
1interface PirateCardProps {
2 name: string;
3 level: number;
4 ship: string;
5}
6
7function PirateCard({ name, level, ship }: PirateCardProps) {
8 return (
9 <div className="border p-4 rounded-lg">
10 <h2 className="text-xl font-bold">{name}</h2>
11 <p>Level: {level}</p>
12 <p>Ship: {ship}</p>
13 </div>
14 );
15}Użycie:
1<PirateCard name="Jack Sparrow" level={10} ship="Black Pearl" />
2<PirateCard name="Anne Bonny" level={8} ship="Revenge" />
3<PirateCard name="Blackbeard" level={12} ship="Queen Anne's Revenge" />Voilà! Jeden komponent, wiele piratów. 🏴☠️
1<PirateCard name="Jack Sparrow" />1<PirateCard level={10} />1<PirateCard isActive={true} />
2
3// Skrót - jeśli true, możesz pominąć wartość
4<PirateCard isActive />1<PirateCard skills={["Navigation", "Combat", "Diplomacy"]} />1<PirateCard
2 pirate={{
3 name: "Jack",
4 level: 10,
5 ship: "Black Pearl"
6 }}
7/>1<PirateCard
2 onLevelUp={() => console.log("Level up!")}
3 onDelete={(id) => deletePirate(id)}
4/>children to specjalny prop - wszystko między tagami komponentu.
1interface CardProps {
2 title: string;
3 children: React.ReactNode;
4}
5
6function Card({ title, children }: CardProps) {
7 return (
8 <div className="border rounded-lg p-6 bg-white shadow-lg">
9 <h2 className="text-2xl font-bold mb-4">{title}</h2>
10 <div className="text-gray-700">
11 {children}
12 </div>
13 </div>
14 );
15}Użycie:
1<Card title="Jack Sparrow">
2 <p>Captain of the Black Pearl</p>
3 <p>Level: 10</p>
4 <button>View Profile</button>
5</Card>
6
7<Card title="Anne Bonny">
8 <img src="/anne.jpg" />
9 <p>Navigator and fighter</p>
10</Card>children może być:
Możesz ustawić wartości domyślne dla props:
1interface ButtonProps {
2 text: string;
3 variant?: "primary" | "secondary"; // Opcjonalne
4 disabled?: boolean;
5}
6
7function Button({
8 text,
9 variant = "primary", // Domyślna wartość
10 disabled = false
11}: ButtonProps) {
12 return (
13 <button
14 disabled={disabled}
15 className={`btn btn-${variant}`}
16 >
17 {text}
18 </button>
19 );
20}
21
22// Użycie
23<Button text="Click me" /> {/* variant = "primary" (default) */}
24<Button text="Secondary" variant="secondary" />
25<Button text="Disabled" disabled />Możesz przekazać wiele props na raz używając spread operator:
1const pirateData = {
2 name: "Jack Sparrow",
3 level: 10,
4 ship: "Black Pearl",
5 isActive: true
6};
7
8// Zamiast:
9<PirateCard
10 name={pirateData.name}
11 level={pirateData.level}
12 ship={pirateData.ship}
13 isActive={pirateData.isActive}
14/>
15
16// Użyj spread:
17<PirateCard {...pirateData} />Oba są równoważne!
Composition to budowanie złożonych UI z prostych komponentów.
1// Małe komponenty (atomic)
2function PostHeader({ author, timestamp }: { author: string; timestamp: string }) {
3 return (
4 <div className="flex items-center gap-3 mb-3">
5 <div className="w-10 h-10 bg-blue-500 rounded-full" />
6 <div>
7 <p className="font-bold">{author}</p>
8 <p className="text-sm text-gray-500">{timestamp}</p>
9 </div>
10 </div>
11 );
12}
13
14function PostContent({ content }: { content: string }) {
15 return <p className="text-gray-800 mb-4">{content}</p>;
16}
17
18function PostActions({ likes, onLike, onShare }: {
19 likes: number;
20 onLike: () => void;
21 onShare: () => void;
22}) {
23 return (
24 <div className="flex gap-4">
25 <button onClick={onLike} className="text-blue-600">
26 👍 {likes}
27 </button>
28 <button onClick={onShare} className="text-blue-600">
29 🔗 Share
30 </button>
31 </div>
32 );
33}
34
35// Composition - składamy z małych komponentów
36function Post({
37 author,
38 timestamp,
39 content,
40 likes,
41 onLike,
42 onShare
43}: {
44 author: string;
45 timestamp: string;
46 content: string;
47 likes: number;
48 onLike: () => void;
49 onShare: () => void;
50}) {
51 return (
52 <div className="border rounded-lg p-4 bg-white shadow">
53 <PostHeader author={author} timestamp={timestamp} />
54 <PostContent content={content} />
55 <PostActions likes={likes} onLike={onLike} onShare={onShare} />
56 </div>
57 );
58}Zalety composition:
PostHeader możesz użyć gdzie indziej)PostActions na inny komponent)Props drilling to przekazywanie props przez wiele poziomów komponentów.
1function App() {
2 const [user, setUser] = useState({ name: "Jack", level: 10 });
3
4 return <Dashboard user={user} />;
5}
6
7function Dashboard({ user }: { user: User }) {
8 return <Sidebar user={user} />;
9}
10
11function Sidebar({ user }: { user: User }) {
12 return <UserProfile user={user} />;
13}
14
15function UserProfile({ user }: { user: User }) {
16 return <div>{user.name}</div>;
17}Problem:
user przechodzi przez Dashboard i Sidebar tylko po to, żeby dotrzeć do UserProfile. One same nie używają user.1function App() {
2 const [user, setUser] = useState({ name: "Jack", level: 10 });
3
4 return (
5 <Dashboard>
6 <Sidebar>
7 <UserProfile user={user} />
8 </Sidebar>
9 </Dashboard>
10 );
11}
12
13function Dashboard({ children }: { children: React.ReactNode }) {
14 return <div className="dashboard">{children}</div>;
15}
16
17function Sidebar({ children }: { children: React.ReactNode }) {
18 return <aside className="sidebar">{children}</aside>;
19}Teraz
Dashboard i Sidebar nie muszą znać user!Gdy props drilling jest nieunikniony, użyj Context API - przekażesz dane "przez powietrze" bezpośrednio do komponentu, który ich potrzebuje.
Nauczymy się tego w Module 5!
Compound components to komponenty które działają razem, dzieląc stan.
1interface TabsContextType {
2 activeTab: string;
3 setActiveTab: (tab: string) => void;
4}
5
6const TabsContext = React.createContext<TabsContextType | null>(null);
7
8function Tabs({ defaultTab, children }: {
9 defaultTab: string;
10 children: React.ReactNode
11}) {
12 const [activeTab, setActiveTab] = useState(defaultTab);
13
14 return (
15 <TabsContext.Provider value={{ activeTab, setActiveTab }}>
16 <div className="tabs">
17 {children}
18 </div>
19 </TabsContext.Provider>
20 );
21}
22
23function TabList({ children }: { children: React.ReactNode }) {
24 return <div className="flex border-b">{children}</div>;
25}
26
27function Tab({ value, children }: { value: string; children: React.ReactNode }) {
28 const context = React.useContext(TabsContext);
29 if (!context) throw new Error("Tab must be used within Tabs");
30
31 const { activeTab, setActiveTab } = context;
32 const isActive = activeTab === value;
33
34 return (
35 <button
36 onClick={() => setActiveTab(value)}
37 className={`px-4 py-2 ${isActive ? 'border-b-2 border-blue-600 font-bold' : ''}`}
38 >
39 {children}
40 </button>
41 );
42}
43
44function TabPanel({ value, children }: { value: string; children: React.ReactNode }) {
45 const context = React.useContext(TabsContext);
46 if (!context) throw new Error("TabPanel must be used within Tabs");
47
48 const { activeTab } = context;
49 if (activeTab !== value) return null;
50
51 return <div className="p-4">{children}</div>;
52}
53
54// Eksportuj jako compound component
55Tabs.List = TabList;
56Tabs.Tab = Tab;
57Tabs.Panel = TabPanel;
58
59// Użycie - super czytelne API!
60<Tabs defaultTab="posts">
61 <Tabs.List>
62 <Tabs.Tab value="posts">Posts</Tabs.Tab>
63 <Tabs.Tab value="crew">Crew</Tabs.Tab>
64 <Tabs.Tab value="ships">Ships</Tabs.Tab>
65 </Tabs.List>
66
67 <Tabs.Panel value="posts">
68 <PostsList />
69 </Tabs.Panel>
70 <Tabs.Panel value="crew">
71 <CrewList />
72 </Tabs.Panel>
73 <Tabs.Panel value="ships">
74 <ShipsList />
75 </Tabs.Panel>
76</Tabs>Zalety compound components:
To zaawansowany pattern - nie musisz go używać od razu, ale warto znać!
Render props to funkcja przekazana jako prop, która zwraca JSX.
1interface MousePosition {
2 x: number;
3 y: number;
4}
5
6function MouseTracker({ render }: { render: (pos: MousePosition) => JSX.Element }) {
7 const [position, setPosition] = useState({ x: 0, y: 0 });
8
9 useEffect(() => {
10 const handleMove = (e: MouseEvent) => {
11 setPosition({ x: e.clientX, y: e.clientY });
12 };
13
14 window.addEventListener('mousemove', handleMove);
15 return () => window.removeEventListener('mousemove', handleMove);
16 }, []);
17
18 return render(position);
19}
20
21// Użycie
22<MouseTracker
23 render={(pos) => (
24 <div>
25 Mouse position: {pos.x}, {pos.y}
26 </div>
27 )}
28/>
29
30<MouseTracker
31 render={(pos) => (
32 <div
33 style={{
34 position: 'fixed',
35 left: pos.x,
36 top: pos.y
37 }}
38 >
39 🏴☠️
40 </div>
41 )}
42/>Render props pozwalają reużywać logikę, ale zmieniać renderowanie!
Uwaga: Dzisiaj częściej używamy custom hooks zamiast render props.
TypeScript wymusza poprawne props - to świetne!
1interface PirateCardProps {
2 name: string;
3 level: number;
4 ship: string;
5}
6
7// ❌ Error: Property 'ship' is missing
8<PirateCard name="Jack" level={10} />
9
10// ❌ Error: Type 'string' is not assignable to type 'number'
11<PirateCard name="Jack" level="10" ship="Pearl" />
12
13// ✅ OK
14<PirateCard name="Jack" level={10} ship="Pearl" />1interface CardProps {
2 title: string;
3 subtitle?: string; // Opcjonalne
4 footer?: React.ReactNode; // Opcjonalne
5}
6
7function Card({ title, subtitle, footer }: CardProps) {
8 return (
9 <div className="card">
10 <h2>{title}</h2>
11 {subtitle && <p>{subtitle}</p>} {/* Renderuj tylko jeśli istnieje */}
12 {footer && <div className="footer">{footer}</div>}
13 </div>
14 );
15}Zbudujmy feed postów używając composition:
1// Types
2interface Post {
3 id: number;
4 author: string;
5 content: string;
6 likes: number;
7 timestamp: string;
8}
9
10// Small reusable components
11function Avatar({ name }: { name: string }) {
12 return (
13 <div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white font-bold text-lg">
14 {name[0]}
15 </div>
16 );
17}
18
19function Button({
20 children,
21 onClick,
22 variant = "primary"
23}: {
24 children: React.ReactNode;
25 onClick: () => void;
26 variant?: "primary" | "secondary";
27}) {
28 const styles = variant === "primary"
29 ? "bg-blue-600 hover:bg-blue-700 text-white"
30 : "bg-gray-200 hover:bg-gray-300 text-gray-800";
31
32 return (
33 <button
34 onClick={onClick}
35 className={`px-4 py-2 rounded-lg font-medium transition ${styles}`}
36 >
37 {children}
38 </button>
39 );
40}
41
42// Post Card component
43function PostCard({ post, onLike, onDelete }: {
44 post: Post;
45 onLike: (id: number) => void;
46 onDelete: (id: number) => void;
47}) {
48 return (
49 <div className="bg-white border rounded-lg p-6 shadow-sm">
50 {/* Header */}
51 <div className="flex items-start gap-3 mb-4">
52 <Avatar name={post.author} />
53 <div className="flex-1">
54 <h3 className="font-bold">{post.author}</h3>
55 <p className="text-sm text-gray-500">{post.timestamp}</p>
56 </div>
57 <button
58 onClick={() => onDelete(post.id)}
59 className="text-gray-400 hover:text-red-600"
60 >
61 ✕
62 </button>
63 </div>
64
65 {/* Content */}
66 <p className="text-gray-800 mb-4">{post.content}</p>
67
68 {/* Actions */}
69 <div className="flex gap-2">
70 <Button onClick={() => onLike(post.id)}>
71 👍 {post.likes}
72 </Button>
73 <Button variant="secondary" onClick={() => alert("Share!")}>
74 🔗 Share
75 </Button>
76 </div>
77 </div>
78 );
79}
80
81// Feed component
82function PirateFeed() {
83 const [posts, setPosts] = useState<Post[]>([
84 {
85 id: 1,
86 author: "Jack Sparrow",
87 content: "Just found the treasure of Isla de Muerta! 🏴☠️💰",
88 likes: 42,
89 timestamp: "2 hours ago"
90 },
91 {
92 id: 2,
93 author: "Anne Bonny",
94 content: "Setting sail for new adventures. Who's joining the crew?",
95 likes: 27,
96 timestamp: "5 hours ago"
97 }
98 ]);
99
100 const handleLike = (id: number) => {
101 setPosts(posts.map(post =>
102 post.id === id
103 ? { ...post, likes: post.likes + 1 }
104 : post
105 ));
106 };
107
108 const handleDelete = (id: number) => {
109 setPosts(posts.filter(post => post.id !== id));
110 };
111
112 return (
113 <div className="max-w-2xl mx-auto p-4 space-y-4">
114 <h1 className="text-3xl font-bold mb-6">🐙 Kraken's Call Feed</h1>
115
116 {posts.map(post => (
117 <PostCard
118 key={post.id}
119 post={post}
120 onLike={handleLike}
121 onDelete={handleDelete}
122 />
123 ))}
124 </div>
125 );
126}To przykład dobrego component composition:
Avatar - reużywalny (możesz użyć wszędzie)Button - reużywalny z wariantamiPostCard - enkapsuluje logikę jednego postaPirateFeed - zarządza całą listą✅ Props - przekazywanie danych do komponentów ✅ children - specjalny prop dla zawartości ✅ Default props - wartości domyślne ✅ Spread props -
{...data} przekazuje wszystko
✅ Component Composition - budowanie z małych komponentów
✅ Props drilling - problem przekazywania props przez wiele poziomów
✅ Compound components - komponenty dzielące stan
✅ Render props - funkcje jako props zwracające JSX
✅ TypeScript - wymusza poprawne typy propsNastępne kroki: W kolejnych modułach nauczysz się:
Do zobaczenia! 🚀