Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Props i Component Composition - Reużywalne komponenty

Czas nauczyć się budować reużywalne komponenty - takie, które możesz używać w wielu miejscach z różnymi danymi.

Czym są Props? 📦

Props (properties) to dane przekazywane do komponentu od rodzica (parent).

Przykład bez props:

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?

Ten sam komponent z props:

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. 🏴‍☠️

Props - Różne typy danych

1. String props

1<PirateCard name="Jack Sparrow" />

2. Number props

1<PirateCard level={10} />

3. Boolean props

1<PirateCard isActive={true} />
2
3// Skrót - jeśli true, możesz pominąć wartość
4<PirateCard isActive />

4. Array props

1<PirateCard skills={["Navigation", "Combat", "Diplomacy"]} />

5. Object props

1<PirateCard 
2  pirate={{
3    name: "Jack",
4    level: 10,
5    ship: "Black Pearl"
6  }} 
7/>

6. Function props (callbacks)

1<PirateCard 
2  onLevelUp={() => console.log("Level up!")} 
3  onDelete={(id) => deletePirate(id)}
4/>

Children prop - Komponenty z zawartością

children to specjalny prop - wszystko między tagami komponentu.

Przykład - Card komponent:

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ć:

  • Tekst
  • JSX elementy
  • Inne komponenty
  • Tablice elementów
  • Null/undefined (nic)

Default props - Wartości domyślne

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 />

Spread props - Przekazywanie wielu props

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!

Component Composition - Budowanie z klocków 🧱

Composition to budowanie złożonych UI z prostych komponentów.

Przykład - Post komponent:

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:

  • ✅ Łatwiejsze testowanie małych komponentów
  • ✅ Reużywalność (
    PostHeader
    możesz użyć gdzie indziej)
  • ✅ Łatwiejsze czytanie kodu
  • ✅ Flexibilność (możesz wymienić
    PostActions
    na inny komponent)

Props drilling - Problem i rozwiązanie 🕳️

Props drilling to przekazywanie props przez wiele poziomów komponentów.

Problem:

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
.

Rozwiązanie 1: Component Composition

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
!

Rozwiązanie 2: Context API (późniejszy temat)

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 - Zaawansowany pattern 🎭

Compound components to komponenty które działają razem, dzieląc stan.

Przykład - Tabs komponent:

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:

  • ✅ Elastyczne API
  • ✅ Czytelny kod użycia
  • ✅ Komponenty dzielą stan bez props drilling
  • ✅ Możesz dowolnie komponować

To zaawansowany pattern - nie musisz go używać od razu, ale warto znać!

Render props pattern - Elastyczność 🎨

Render props to funkcja przekazana jako prop, która zwraca JSX.

Przykład - Mouse tracker:

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.

Props validation z TypeScript 🛡️

TypeScript wymusza poprawne props - to świetne!

Przykład błędu:

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" />

Opcjonalne props:

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}

Praktyczny przykład - Pirate Feed 🏴‍☠️

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        >
6162        </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 wariantami
  • PostCard
    - enkapsuluje logikę jednego posta
  • PirateFeed
    - zarządza całą listą

Podsumowanie 🎓

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 props

Następne kroki: W kolejnych modułach nauczysz się:

  • Context API (rozwiązanie props drilling)
  • Custom hooks (zamiast render props)
  • Server Components (Next.js specific)

Do zobaczenia! 🚀

Przejdź do CodeWorlds