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

State Management - useState i interaktywność

Czas dodać interaktywność do Kraken's Call! Nauczysz się zarządzać stanem aplikacji i tworzyć formularze.

useState - Hook do zarządzania stanem

useState to React hook który pozwala komponentom "pamiętać" rzeczy.

Podstawowe użycie:

1'use client';
2
3import { useState } from 'react';
4
5export default function Counter() {
6  // [wartość, funkcja do zmiany] = useState(wartość początkowa)
7  const [count, setCount] = useState(0);
8
9  return (
10    <div>
11      <p>Licznik: {count}</p>
12      <button onClick={() => setCount(count + 1)}>
13        +1
14      </button>
15    </div>
16  );
17}

Jak to działa:

  1. useState(0)
    - tworzy stan z wartością początkową 0
  2. count
    - aktualna wartość
  3. setCount
    - funkcja do zmiany wartości
  4. Gdy klikniesz przycisk →
    setCount
    zmienia
    count
    → komponent re-renderuje się

Różne typy stanu

String:

1const [name, setName] = useState('');
2
3<input
4  value={name}
5  onChange={(e) => setName(e.target.value)}
6/>

Boolean:

1const [isOpen, setIsOpen] = useState(false);
2
3<button onClick={() => setIsOpen(!isOpen)}>
4  Toggle
5</button>

Array:

1const [items, setItems] = useState<string[]>([]);
2
3const addItem = (item: string) => {
4  setItems([...items, item]);
5};

Object:

1const [user, setUser] = useState({
2  name: 'Redbeard',
3  age: 35
4});
5
6setUser({ ...user, age: 36 });

Formularz dodawania posta

Stwórzmy formularz do dodawania nowych postów:

1'use client';
2
3import { useState } from 'react';
4
5export default function CreatePostForm() {
6  const [content, setContent] = useState('');
7  const [charCount, setCharCount] = useState(0);
8  const MAX_LENGTH = 280;
9
10  const handleSubmit = (e: React.FormEvent) => {
11    e.preventDefault(); // Zapobiega przeładowaniu strony
12
13    if (content.trim()) {
14      console.log('Nowy post:', content);
15      setContent(''); // Wyczyść formularz
16      setCharCount(0);
17    }
18  };
19
20  const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
21    const text = e.target.value;
22    setContent(text);
23    setCharCount(text.length);
24  };
25
26  return (
27    <form onSubmit={handleSubmit} className="bg-white p-6 rounded-lg shadow">
28      <textarea
29        value={content}
30        onChange={handleChange}
31        maxLength={MAX_LENGTH}
32        className="w-full p-4 border rounded-lg resize-none focus:ring-2 focus:ring-blue-500 outline-none"
33        rows={4}
34      />
35
36      <div className="flex items-center justify-between mt-4">
37        <span className={`text-sm ${
38          charCount > MAX_LENGTH * 0.9 ? 'text-red-500' : 'text-gray-500'
39        }`}>
40          {charCount}/{MAX_LENGTH}
41        </span>
42
43        <button
44          type="submit"
45          disabled={!content.trim() || charCount > MAX_LENGTH}
46          className="bg-blue-600 text-white px-6 py-2 rounded-lg font-semibold disabled:bg-gray-300 disabled:cursor-not-allowed hover:bg-blue-700 transition"
47        >
48          Wyślij 🚀
49        </button>
50      </div>
51    </form>
52  );
53}

Feed z dynamicznymi postami

Teraz połączmy formularz z listą postów:

1'use client';
2
3import { useState } from 'react';
4
5interface Post {
6  id: number;
7  author: string;
8  avatar: string;
9  content: string;
10  timestamp: Date;
11  likes: number;
12}
13
14export default function Feed() {
15  const [posts, setPosts] = useState<Post[]>([
16    {
17      id: 1,
18      author: 'Kapitan Redbeard',
19      avatar: '🏴‍☠️',
20      content: 'Właśnie odkryliśmy nową wyspę!',
21      timestamp: new Date(),
22      likes: 42,
23    },
24  ]);
25
26  const [newPostContent, setNewPostContent] = useState('');
27
28  const handleSubmit = (e: React.FormEvent) => {
29    e.preventDefault();
30
31    if (!newPostContent.trim()) return;
32
33    const newPost: Post = {
34      id: Date.now(),
35      author: 'Morgan Blackheart',
36      avatar: '⚓',
37      content: newPostContent,
38      timestamp: new Date(),
39      likes: 0,
40    };
41
42    setPosts([newPost, ...posts]); // Dodaj na początek
43    setNewPostContent('');
44  };
45
46  return (
47    <div className="max-w-2xl mx-auto p-8">
48      {/* Formularz */}
49      <form onSubmit={handleSubmit} className="bg-white p-6 rounded-lg shadow mb-6">
50        <textarea
51          value={newPostContent}
52          onChange={(e) => setNewPostContent(e.target.value)}
53          className="w-full p-4 border rounded-lg"
54          rows={3}
55        />
56        <button
57          type="submit"
58          className="mt-4 bg-blue-600 text-white px-6 py-2 rounded-lg"
59        >
60          Wyślij
61        </button>
62      </form>
63
64      {/* Lista postów */}
65      <div className="space-y-4">
66        {posts.map((post) => (
67          <div key={post.id} className="bg-white p-6 rounded-lg shadow">
68            <div className="flex items-center gap-3 mb-4">
69              <div className="text-3xl">{post.avatar}</div>
70              <div>
71                <h3 className="font-bold">{post.author}</h3>
72                <p className="text-sm text-gray-500">
73                  {post.timestamp.toLocaleTimeString('pl-PL')}
74                </p>
75              </div>
76            </div>
77            <p className="text-gray-800">{post.content}</p>
78            <div className="mt-4 text-gray-500">
79              ❤️ {post.likes}
80            </div>
81          </div>
82        ))}
83      </div>
84    </div>
85  );
86}

Lifting State Up - Dzielenie stanu między komponentami

Gdy dwa komponenty potrzebują tego samego stanu, przenosimy go do wspólnego rodzica.

1'use client';
2
3import { useState } from 'react';
4
5// Komponent przycisku Like
6function LikeButton({
7  likes,
8  onLike
9}: {
10  likes: number;
11  onLike: () => void;
12}) {
13  return (
14    <button onClick={onLike} className="flex items-center gap-2">
15      ❤️ {likes}
16    </button>
17  );
18}
19
20// Komponent licznika polubień
21function LikeCounter({ likes }: { likes: number }) {
22  return (
23    <div className="text-sm text-gray-500">
24      Łącznie polubień na stronie: {likes}
25    </div>
26  );
27}
28
29// Rodzic przechowuje stan
30export default function Post() {
31  const [likes, setLikes] = useState(42);
32
33  const handleLike = () => {
34    setLikes(likes + 1);
35  };
36
37  return (
38    <div className="bg-white p-6 rounded-lg">
39      <p className="mb-4">Treść posta...</p>
40
41      <LikeButton likes={likes} onLike={handleLike} />
42      <LikeCounter likes={likes} />
43    </div>
44  );
45}

useEffect - Side effects

useEffect
uruchamia kod po renderze (np. fetchowanie danych, timery).

1'use client';
2
3import { useState, useEffect } from 'react';
4
5export default function Timer() {
6  const [seconds, setSeconds] = useState(0);
7
8  useEffect(() => {
9    // Uruchom po każdym renderze
10    const interval = setInterval(() => {
11      setSeconds((s) => s + 1);
12    }, 1000);
13
14    // Cleanup - usuń timer gdy komponent zniknie
15    return () => clearInterval(interval);
16  }, []); // [] = uruchom tylko raz
17
18  return <div>Upłynęło: {seconds}s</div>;
19}

Dependency array:

1// Uruchom tylko raz (przy montowaniu)
2useEffect(() => {
3  console.log('Component mounted');
4}, []);
5
6// Uruchom gdy count się zmieni
7useEffect(() => {
8  console.log('Count changed:', count);
9}, [count]);
10
11// Uruchom po każdym renderze
12useEffect(() => {
13  console.log('Every render');
14}); // Brak []

LocalStorage - Zapisywanie stanu

Zapisz posty w przeglądarce żeby nie zniknęły po odświeżeniu:

1'use client';
2
3import { useState, useEffect } from 'react';
4
5export default function Feed() {
6  const [posts, setPosts] = useState<Post[]>([]);
7
8  // Wczytaj z localStorage przy starcie
9  useEffect(() => {
10    const saved = localStorage.getItem('kraken-posts');
11    if (saved) {
12      setPosts(JSON.parse(saved));
13    }
14  }, []);
15
16  // Zapisz do localStorage gdy posts się zmieni
17  useEffect(() => {
18    localStorage.setItem('kraken-posts', JSON.stringify(posts));
19  }, [posts]);
20
21  // Reszta kodu...
22}

Podsumowanie 🎓

useState - hook do zarządzania stanem ✅ Formularze -

value
,
onChange
,
onSubmit
e.preventDefault() - zapobiega przeładowaniu strony ✅ Dynamiczne listy - dodawanie/usuwanie elementów ✅ Lifting state up - dzielenie stanu między komponentami ✅ useEffect - side effects (timery, fetch, localStorage) ✅ Dependency array - kontrola kiedy effect się uruchamia ✅ localStorage - zapisywanie danych w przeglądarce

Teraz masz kompletną, interaktywną aplikację Kraken's Call! 🎉

Do zobaczenia! 🚀

Przejdź do CodeWorlds