You've learned
useState for state management. Now you'll learn useRef - a hook for creating references to DOM elements and storing values that don't cause re-renders.useRef is a hook that returns a ref object with a
.current property. This object survives between re-renders but changing .current doesn't cause a re-render.1import { useRef } from 'react';1const myRef = useRef(initialValue);
2
3// Read value
4console.log(myRef.current);
5
6// Change value
7myRef.current = newValue;| Feature | useState | useRef | |---------|----------|--------| | Re-rendering | ✅ Causes re-render | ❌ Doesn't cause re-render | | Persistence | ✅ Between renders | ✅ Between renders | | Usage | UI data | DOM references, helper values | | Syntax |
[value, setValue] | .current |1function Counter() {
2 const [count, setCount] = useState(0); // useState
3 const renderCount = useRef(0); // useRef
4
5 // Increment renderCount on every render
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}What happens:
count changesrenderCount.current increases WITHOUT causing an additional re-renderResult:
Most common use of useRef - direct access to DOM elements.
1function SearchForm() {
2 const inputRef = useRef<HTMLInputElement>(null);
3
4 const handleFocus = () => {
5 // Direct DOM method call
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 on input
17 </button>
18 </div>
19 );
20}Explanation:
inputRef.current is a reference to the <input> element.focus(), .blur(), .scrollIntoView()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 {/* Long list of posts... */}
15 <div className="space-y-4">
16 {posts.map(post => <PostCard key={post.id} post={post} />)}
17 </div>
18
19 {/* Scroll to top button */}
20 <button
21 onClick={scrollToTop}
22 className="fixed bottom-4 right-4 bg-blue-600 text-white p-4 rounded-full"
23 >
24 ↑
25 </button>
26 </div>
27 );
28}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>Width: {dimensions.width}px</p>
15 <p>Height: {dimensions.height}px</p>
16 </div>
17 );
18}In React there are two ways to handle forms: controlled and uncontrolled.
Input value controlled by 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} // ← Controlled by state
14 onChange={(e) => setEmail(e.target.value)} // ← Synchronization
15 />
16 <button type="submit">Submit</button>
17 </form>
18 );
19}Advantages of controlled:
Disadvantages of controlled:
Input value managed by DOM, we read it only on 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; // ← Read from DOM
7 console.log("Email:", email);
8 };
9
10 return (
11 <form onSubmit={handleSubmit}>
12 <input
13 ref={emailRef} // ← Ref instead of value/onChange
14 type="email"
15 defaultValue="" // ← defaultValue instead of value
16 />
17 <button type="submit">Submit</button>
18 </form>
19 );
20}Advantages of uncontrolled:
Disadvantages of uncontrolled:
Controlled:
Uncontrolled:
<input type="file"> is always uncontrolled!)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}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 // Validation on 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}<input type="file"> is always uncontrolled - React can't control files for security reasons.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 // Image preview
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 to 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 is great for storing values that don't affect UI but need to survive between renders.
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// Usage
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}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; // Already running
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 on 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}Sometimes you need access to the current state value in an event:
1function Chat() {
2 const [messages, setMessages] = useState<string[]>([]);
3 const messagesRef = useRef(messages);
4
5 // Synchronize ref with 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 // ❌ This would use the old messages value from closure
16 // setMessages([...messages, event.data]);
17
18 // ✅ This uses the current value
19 setMessages([...messagesRef.current, event.data]);
20 };
21
22 return () => ws.close();
23 }, []); // Empty array - effect only once!
24
25 return (
26 <div>
27 {messages.map((msg, i) => <p key={i}>{msg}</p>)}
28 </div>
29 );
30}Sometimes you want to pass a ref to a child component.
1function MyInput(props) {
2 return <input {...props} />;
3}
4
5// ❌ This won't work!
6const ref = useRef();
7<MyInput ref={ref} />Why?
ref is not a regular prop - React blocks it.1import { forwardRef } from 'react';
2
3const MyInput = forwardRef<HTMLInputElement, { placeholder?: string }>(
4 (props, ref) => {
5 return <input ref={ref} {...props} />;
6 }
7);
8
9// ✅ Now it works!
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 lets you customize what's available through 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// Usage
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 limits the API - the parent only sees
play, pause, seek, not the entire video element.✅ useRef - hook for creating references, doesn't cause re-render ✅ useRef(initialValue) - returns
{ current: initialValue }
✅ ref={myRef} - assigns DOM element to .current
✅ Controlled components - value controlled by state (value + onChange)
✅ Uncontrolled components - value in DOM, read by useRef
✅ File inputs - always uncontrolled
✅ useRef for values - storing values without re-render (timers, previous values)
✅ forwardRef - passing ref to components
✅ useImperativeHandle - custom ref APIWhen to use:
See you in Module 3! 🚀