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

useRef - Referencje i Uncontrolled Components

Poznałeś

useState
do zarządzania stanem. Teraz poznasz useRef - hook do tworzenia referencji (references) do elementów DOM i przechowywania wartości, które nie powodują re-renderingu.

Czym jest useRef? 🎯

useRef to hook, który zwraca obiekt ref z właściwością

.current
. Ten obiekt przeżywa między re-renderingami ale zmiana .current nie powoduje re-renderingu.

Import:

1import { useRef } from 'react';

Podstawowe użycie:

1const myRef = useRef(initialValue);
2
3// Odczyt wartości
4console.log(myRef.current);
5
6// Zmiana wartości
7myRef.current = newValue;

useRef vs useState 🆚

| Feature | useState | useRef | |---------|----------|--------| | Re-rendering | ✅ Powoduje re-render | ❌ Nie powoduje re-render | | Persistence | ✅ Między renderami | ✅ Między renderami | | Użycie | Dane UI | Referencje DOM, wartości pomocnicze | | Składnia |

[value, setValue]
|
.current
|

Przykład różnicy:

1function Counter() {
2  const [count, setCount] = useState(0);  // useState
3  const renderCount = useRef(0);          // useRef
4
5  // Zwiększ renderCount przy każdym renderze
6  renderCount.current = renderCount.current + 1;
7
8  return (
9    <div>
10      <p>Count: {count}</p>
11      <p>Renders: {renderCount.current}</p>
12      <button onClick={() => setCount(count + 1)}>
13        Increment
14      </button>
15    </div>
16  );
17}

Co się dzieje:

  1. Klikniesz przycisk →
    count
    się zmienia
  2. Komponent re-renderuje
  3. renderCount.current
    zwiększa się BEZ powodowania dodatkowego re-renderu

Wynik:

  • Klik 1: Count: 1, Renders: 2
  • Klik 2: Count: 2, Renders: 3

useRef dla elementów DOM 🎨

Najczęstsze użycie useRef - bezpośredni dostęp do elementów DOM.

Przykład 1: Focus na input

1function SearchForm() {
2  const inputRef = useRef<HTMLInputElement>(null);
3
4  const handleFocus = () => {
5    // Bezpośrednie wywołanie metody DOM
6    inputRef.current?.focus();
7  };
8
9  return (
10    <div>
11      <input 
12        ref={inputRef}
13        type="text" 
14      />
15      <button onClick={handleFocus}>
16        Focus na input
17      </button>
18    </div>
19  );
20}

Wyjaśnienie:

  • inputRef.current
    to referencja do elementu
    <input>
  • Możesz wywołać metody DOM jak
    .focus()
    ,
    .blur()
    ,
    .scrollIntoView()

Przykład 2: Scroll do elementu

1function PirateFeed() {
2  const topRef = useRef<HTMLDivElement>(null);
3
4  const scrollToTop = () => {
5    topRef.current?.scrollIntoView({ behavior: 'smooth' });
6  };
7
8  return (
9    <div>
10      <div ref={topRef} className="text-2xl font-bold mb-4">
11        🐙 Kraken's Call Feed
12      </div>
13
14      {/* Długa lista postów... */}
15      <div className="space-y-4">
16        {posts.map(post => <PostCard key={post.id} post={post} />)}
17      </div>
18
19      {/* Przycisk scroll to top */}
20      <button 
21        onClick={scrollToTop}
22        className="fixed bottom-4 right-4 bg-blue-600 text-white p-4 rounded-full"
23      >
2425      </button>
26    </div>
27  );
28}

Przykład 3: Pomiar wymiarów elementu

1function ImageGallery() {
2  const containerRef = useRef<HTMLDivElement>(null);
3  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
4
5  useEffect(() => {
6    if (containerRef.current) {
7      const { width, height } = containerRef.current.getBoundingClientRect();
8      setDimensions({ width, height });
9    }
10  }, []);
11
12  return (
13    <div ref={containerRef} className="gallery">
14      <p>Szerokość: {dimensions.width}px</p>
15      <p>Wysokość: {dimensions.height}px</p>
16    </div>
17  );
18}

Controlled vs Uncontrolled Components 🎛️

W React są dwa sposoby obsługi formularzy: controlled i uncontrolled.

Controlled Components (ze state)

Wartość inputa kontrolowana przez React state:

1function ControlledForm() {
2  const [email, setEmail] = useState("");
3
4  const handleSubmit = (e: React.FormEvent) => {
5    e.preventDefault();
6    console.log("Email:", email);
7  };
8
9  return (
10    <form onSubmit={handleSubmit}>
11      <input 
12        type="email"
13        value={email}                        // ← Kontrolowane przez state
14        onChange={(e) => setEmail(e.target.value)}  // ← Synchronizacja
15      />
16      <button type="submit">Submit</button>
17    </form>
18  );
19}

Zalety controlled:

  • ✅ Możesz walidować na bieżąco
  • ✅ Możesz modyfikować wartość programowo
  • ✅ Możesz łatwo zresetować formularz

Wady controlled:

  • ❌ Więcej kodu (state, onChange)
  • ❌ Re-render przy każdej literze

Uncontrolled Components (z useRef)

Wartość inputa zarządzana przez DOM, odczytujemy ją tylko przy submit:

1function UncontrolledForm() {
2  const emailRef = useRef<HTMLInputElement>(null);
3
4  const handleSubmit = (e: React.FormEvent) => {
5    e.preventDefault();
6    const email = emailRef.current?.value;  // ← Odczyt z DOM
7    console.log("Email:", email);
8  };
9
10  return (
11    <form onSubmit={handleSubmit}>
12      <input 
13        ref={emailRef}                      // ← Ref zamiast value/onChange
14        type="email"
15        defaultValue=""                     // ← defaultValue zamiast value
16      />
17      <button type="submit">Submit</button>
18    </form>
19  );
20}

Zalety uncontrolled:

  • ✅ Mniej kodu
  • ✅ Lepsza wydajność (brak re-renderów)
  • ✅ Łatwiejsza integracja z bibliotekami non-React

Wady uncontrolled:

  • ❌ Brak walidacji na bieżąco
  • ❌ Nie możesz modyfikować wartości programowo
  • ❌ Trudniejszy reset formularza

Kiedy używać którego? 🤔

Controlled:

  • Walidacja na żywo (np. "Email musi zawierać @")
  • Formatowanie na żywo (np. numer telefonu: (123) 456-7890)
  • Conditional logic (np. pokaż inne pole jeśli checkbox zaznaczony)
  • Synchronizacja z innymi komponentami

Uncontrolled:

  • Prosty formularz submit (np. login, newsletter)
  • Lepsza wydajność dla dużych formularzy
  • Integracja z bibliotekami zewnętrznymi
  • File uploads (
    <input type="file">
    zawsze uncontrolled!)

Praktyczny przykład - Pirate Login Form 🏴‍☠️

Wersja Controlled:

1function ControlledLoginForm() {
2  const [username, setUsername] = useState("");
3  const [password, setPassword] = useState("");
4  const [errors, setErrors] = useState<{ username?: string; password?: string }>({});
5
6  const validate = () => {
7    const newErrors: typeof errors = {};
8
9    if (username.length < 3) {
10      newErrors.username = "Username must be at least 3 characters";
11    }
12    if (password.length < 6) {
13      newErrors.password = "Password must be at least 6 characters";
14    }
15
16    setErrors(newErrors);
17    return Object.keys(newErrors).length === 0;
18  };
19
20  const handleSubmit = (e: React.FormEvent) => {
21    e.preventDefault();
22    if (validate()) {
23      console.log("Login:", { username, password });
24      // API call...
25    }
26  };
27
28  return (
29    <form onSubmit={handleSubmit} className="max-w-md mx-auto p-6 space-y-4">
30      <div>
31        <label className="block mb-2 font-medium">Username</label>
32        <input
33          type="text"
34          value={username}
35          onChange={(e) => setUsername(e.target.value)}
36          className="w-full border rounded px-3 py-2"
37        />
38        {errors.username && (
39          <p className="text-red-600 text-sm mt-1">{errors.username}</p>
40        )}
41      </div>
42
43      <div>
44        <label className="block mb-2 font-medium">Password</label>
45        <input
46          type="password"
47          value={password}
48          onChange={(e) => setPassword(e.target.value)}
49          className="w-full border rounded px-3 py-2"
50        />
51        {errors.password && (
52          <p className="text-red-600 text-sm mt-1">{errors.password}</p>
53        )}
54      </div>
55
56      <button 
57        type="submit"
58        className="w-full bg-blue-600 text-white py-2 rounded font-medium hover:bg-blue-700"
59      >
60        Login
61      </button>
62    </form>
63  );
64}

Wersja Uncontrolled:

1function UncontrolledLoginForm() {
2  const usernameRef = useRef<HTMLInputElement>(null);
3  const passwordRef = useRef<HTMLInputElement>(null);
4  const [error, setError] = useState("");
5
6  const handleSubmit = (e: React.FormEvent) => {
7    e.preventDefault();
8    
9    const username = usernameRef.current?.value || "";
10    const password = passwordRef.current?.value || "";
11
12    // Walidacja przy submit
13    if (username.length < 3) {
14      setError("Username must be at least 3 characters");
15      return;
16    }
17    if (password.length < 6) {
18      setError("Password must be at least 6 characters");
19      return;
20    }
21
22    setError("");
23    console.log("Login:", { username, password });
24    // API call...
25  };
26
27  return (
28    <form onSubmit={handleSubmit} className="max-w-md mx-auto p-6 space-y-4">
29      {error && (
30        <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
31          {error}
32        </div>
33      )}
34
35      <div>
36        <label className="block mb-2 font-medium">Username</label>
37        <input
38          ref={usernameRef}
39          type="text"
40          defaultValue=""
41          className="w-full border rounded px-3 py-2"
42        />
43      </div>
44
45      <div>
46        <label className="block mb-2 font-medium">Password</label>
47        <input
48          ref={passwordRef}
49          type="password"
50          defaultValue=""
51          className="w-full border rounded px-3 py-2"
52        />
53      </div>
54
55      <button 
56        type="submit"
57        className="w-full bg-blue-600 text-white py-2 rounded font-medium hover:bg-blue-700"
58      >
59        Login
60      </button>
61    </form>
62  );
63}

File Upload - Zawsze uncontrolled 📁

<input type="file">
zawsze jest uncontrolled - React nie może kontrolować plików z przyczyn bezpieczeństwa.

1function AvatarUpload() {
2  const fileInputRef = useRef<HTMLInputElement>(null);
3  const [preview, setPreview] = useState<string | null>(null);
4
5  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
6    const file = e.target.files?.[0];
7    if (file) {
8      // Podgląd obrazka
9      const reader = new FileReader();
10      reader.onloadend = () => {
11        setPreview(reader.result as string);
12      };
13      reader.readAsDataURL(file);
14    }
15  };
16
17  const handleUpload = () => {
18    const file = fileInputRef.current?.files?.[0];
19    if (file) {
20      console.log("Uploading:", file.name);
21      // Upload do API...
22    }
23  };
24
25  return (
26    <div className="space-y-4">
27      <input 
28        ref={fileInputRef}
29        type="file" 
30        accept="image/*"
31        onChange={handleFileChange}
32        className="block w-full text-sm"
33      />
34
35      {preview && (
36        <img 
37          src={preview} 
38          alt="Preview" 
39          className="w-32 h-32 rounded-full object-cover"
40        />
41      )}
42
43      <button 
44        onClick={handleUpload}
45        className="bg-blue-600 text-white px-4 py-2 rounded"
46      >
47        Upload Avatar
48      </button>
49    </div>
50  );
51}

useRef dla wartości pomocniczych 🧰

useRef świetnie nadaje się do przechowywania wartości, które nie wpływają na UI ale muszą przetrwać między renderami.

Przykład 1: Poprzednia wartość state

1function usePrevious<T>(value: T): T | undefined {
2  const ref = useRef<T>();
3  
4  useEffect(() => {
5    ref.current = value;
6  }, [value]);
7  
8  return ref.current;
9}
10
11// Użycie
12function Counter() {
13  const [count, setCount] = useState(0);
14  const prevCount = usePrevious(count);
15
16  return (
17    <div>
18      <p>Current: {count}</p>
19      <p>Previous: {prevCount}</p>
20      <button onClick={() => setCount(count + 1)}>
21        Increment
22      </button>
23    </div>
24  );
25}

Przykład 2: Interval/Timeout IDs

1function Timer() {
2  const [seconds, setSeconds] = useState(0);
3  const intervalRef = useRef<NodeJS.Timeout | null>(null);
4
5  const start = () => {
6    if (intervalRef.current) return;  // Już działa
7
8    intervalRef.current = setInterval(() => {
9      setSeconds(s => s + 1);
10    }, 1000);
11  };
12
13  const stop = () => {
14    if (intervalRef.current) {
15      clearInterval(intervalRef.current);
16      intervalRef.current = null;
17    }
18  };
19
20  const reset = () => {
21    stop();
22    setSeconds(0);
23  };
24
25  // Cleanup przy unmount
26  useEffect(() => {
27    return () => stop();
28  }, []);
29
30  return (
31    <div>
32      <p className="text-4xl font-bold">{seconds}s</p>
33      <div className="space-x-2">
34        <button onClick={start}>Start</button>
35        <button onClick={stop}>Stop</button>
36        <button onClick={reset}>Reset</button>
37      </div>
38    </div>
39  );
40}

Przykład 3: Callback ref - dynamiczne wartości

Czasami potrzebujesz dostępu do aktualnej wartości state w evencie:

1function Chat() {
2  const [messages, setMessages] = useState<string[]>([]);
3  const messagesRef = useRef(messages);
4
5  // Synchronizuj ref z state
6  useEffect(() => {
7    messagesRef.current = messages;
8  }, [messages]);
9
10  useEffect(() => {
11    // WebSocket callback
12    const ws = new WebSocket('ws://localhost:8080');
13    
14    ws.onmessage = (event) => {
15      // ❌ To by użyło starej wartości messages z closure
16      // setMessages([...messages, event.data]);
17
18      // ✅ To używa aktualnej wartości
19      setMessages([...messagesRef.current, event.data]);
20    };
21
22    return () => ws.close();
23  }, []);  // Pusty array - effect tylko raz!
24
25  return (
26    <div>
27      {messages.map((msg, i) => <p key={i}>{msg}</p>)}
28    </div>
29  );
30}

Forwarding Refs - Przekazywanie ref do komponentów 🔗

Czasami chcesz przekazać ref do komponentu dziecka.

Problem:

1function MyInput(props) {
2  return <input {...props} />;
3}
4
5// ❌ To nie zadziała!
6const ref = useRef();
7<MyInput ref={ref} />

Dlaczego?

ref
nie jest zwykłym propem - React go blokuje.

Rozwiązanie: forwardRef

1import { forwardRef } from 'react';
2
3const MyInput = forwardRef<HTMLInputElement, { placeholder?: string }>(
4  (props, ref) => {
5    return <input ref={ref} {...props} />;
6  }
7);
8
9// ✅ Teraz działa!
10function Form() {
11  const inputRef = useRef<HTMLInputElement>(null);
12
13  return (
14    <div>
15      <MyInput ref={inputRef} placeholder="Type here..." />
16      <button onClick={() => inputRef.current?.focus()}>
17        Focus
18      </button>
19    </div>
20  );
21}

useImperativeHandle - Custom ref API 🎛️

useImperativeHandle
pozwala dostosować co jest dostępne przez ref.

1import { useImperativeHandle, forwardRef, useRef } from 'react';
2
3interface VideoPlayerRef {
4  play: () => void;
5  pause: () => void;
6  seek: (time: number) => void;
7}
8
9const VideoPlayer = forwardRef<VideoPlayerRef, { src: string }>((props, ref) => {
10  const videoRef = useRef<HTMLVideoElement>(null);
11
12  useImperativeHandle(ref, () => ({
13    play: () => videoRef.current?.play(),
14    pause: () => videoRef.current?.pause(),
15    seek: (time: number) => {
16      if (videoRef.current) {
17        videoRef.current.currentTime = time;
18      }
19    }
20  }));
21
22  return <video ref={videoRef} src={props.src} />;
23});
24
25// Użycie
26function App() {
27  const playerRef = useRef<VideoPlayerRef>(null);
28
29  return (
30    <div>
31      <VideoPlayer ref={playerRef} src="/video.mp4" />
32      
33      <button onClick={() => playerRef.current?.play()}>Play</button>
34      <button onClick={() => playerRef.current?.pause()}>Pause</button>
35      <button onClick={() => playerRef.current?.seek(30)}>
36        Skip to 30s
37      </button>
38    </div>
39  );
40}

useImperativeHandle ogranicza API - parent widzi tylko

play
,
pause
,
seek
, nie cały element video.

Podsumowanie 🎓

useRef - hook do tworzenia referencji, nie powoduje re-render ✅ useRef(initialValue) - zwraca

{ current: initialValue }
ref={myRef} - przypisuje element DOM do
.current
Controlled components - wartość kontrolowana przez state (value + onChange) ✅ Uncontrolled components - wartość w DOM, odczytana przez useRef ✅ File inputs - zawsze uncontrolled ✅ useRef dla wartości - przechowywanie wartości bez re-renderu (timers, previous values) ✅ forwardRef - przekazywanie ref do komponentów ✅ useImperativeHandle - custom ref API

Kiedy używać:

  • useState - dla danych UI, które powodują re-render
  • useRef (DOM) - dla dostępu do elementów DOM
  • useRef (wartości) - dla wartości pomocniczych (timers, previous state)

Do zobaczenia w Module 3! 🚀

Przejdź do CodeWorlds