We use cookies to enhance your experience on the site
CodeWorlds

useRef - References and Uncontrolled Components

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.

What is useRef? 🎯

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.

Import:

1import { useRef } from 'react';

Basic usage:

1const myRef = useRef(initialValue);
2
3// Read value
4console.log(myRef.current);
5
6// Change value
7myRef.current = newValue;

useRef vs useState 🆚

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

Example of the difference:

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:

  1. You click the button →
    count
    changes
  2. Component re-renders
  3. renderCount.current
    increases WITHOUT causing an additional re-render

Result:

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

useRef for DOM Elements 🎨

Most common use of useRef - direct access to DOM elements.

Example 1: Focus on input

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
  • You can call DOM methods like
    .focus()
    ,
    .blur()
    ,
    .scrollIntoView()

Example 2: Scroll to element

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      >
2425      </button>
26    </div>
27  );
28}

Example 3: Measuring element dimensions

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}

Controlled vs Uncontrolled Components 🎛️

In React there are two ways to handle forms: controlled and uncontrolled.

Controlled Components (with state)

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:

  • ✅ You can validate in real time
  • ✅ You can modify the value programmatically
  • ✅ You can easily reset the form

Disadvantages of controlled:

  • ❌ More code (state, onChange)
  • ❌ Re-render on every keystroke

Uncontrolled Components (with useRef)

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:

  • ✅ Less code
  • ✅ Better performance (no re-renders)
  • ✅ Easier integration with non-React libraries

Disadvantages of uncontrolled:

  • ❌ No real-time validation
  • ❌ Can't modify value programmatically
  • ❌ Harder form reset

When to use which? 🤔

Controlled:

  • Live validation (e.g. "Email must contain @")
  • Live formatting (e.g. phone number: (123) 456-7890)
  • Conditional logic (e.g. show another field if checkbox is checked)
  • Synchronization with other components

Uncontrolled:

  • Simple form submit (e.g. login, newsletter)
  • Better performance for large forms
  • Integration with external libraries
  • File uploads (
    <input type="file">
    is always uncontrolled!)

Practical Example - Pirate Login Form 🏴‍☠️

Controlled version:

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}

Uncontrolled version:

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}

File Upload - Always Uncontrolled 📁

<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 for Helper Values 🧰

useRef is great for storing values that don't affect UI but need to survive between renders.

Example 1: Previous state value

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}

Example 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;  // 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}

Example 3: Callback ref - dynamic values

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}

Forwarding Refs - Passing ref to Components 🔗

Sometimes you want to pass a ref to a child component.

Problem:

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.

Solution: 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// ✅ 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 - Custom ref API 🎛️

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.

Summary 🎓

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 API

When to use:

  • useState - for UI data that causes re-render
  • useRef (DOM) - for accessing DOM elements
  • useRef (values) - for helper values (timers, previous state)

See you in Module 3! 🚀

Go to CodeWorlds