On our cosmic journey through React, sometimes we need to maintain certain data or references to DOM elements that persist between renders but don't cause the component to re-render. The
useRef hook is like a storage compartment on a spaceship - it lets you store important things that don't change with every maneuver of the ship.The
useRef hook is one of the built-in hooks in React that:Basic syntax:
1const refContainer = useRef(initialValue);The returned object
refContainer has a .current property that is initialized with the initial value (initialValue). This value can be changed without triggering a re-render.One of the most popular uses of
useRef is gaining direct access to DOM nodes:1function SpaceshipControls() {
2 // Creating reference
3 const thrusterControlRef = useRef(null);
4
5 // Function using reference to DOM element
6 const focusThrusterControl = () => {
7 // Access DOM element through .current property
8 thrusterControlRef.current.focus();
9 };
10
11 return (
12 <div className="control-panel">
13 <h2>Thruster Control Panel</h2>
14
15 {/* Przypisanie referencji do elementu input */}
16 <input
17 ref={thrusterControlRef}
18 type="range"
19 min="0"
20 max="100"
21 className="thruster-slider"
22 />
23
24 <button onClick={focusThrusterControl}>
25 Activate thrust control
26 </button>
27 </div>
28 );
29}In this example:
thrusterControlRef using useRef(null)input element using the ref attributefocus() method on the DOM elementuseRef can also be used to store values that:1function MissionTimer() {
2 const [seconds, setSeconds] = useState(0);
3 const timerIdRef = useRef(null);
4
5 // Function to start the timer
6 const startTimer = () => {
7 if (timerIdRef.current !== null) return; // Avoid duplicate timers
8
9 timerIdRef.current = setInterval(() => {
10 setSeconds(s => s + 1);
11 }, 1000);
12 };
13
14 // Function to stop the timer
15 const stopTimer = () => {
16 clearInterval(timerIdRef.current);
17 timerIdRef.current = null;
18 };
19
20 // Clean up interval on unmount
21 useEffect(() => {
22 return () => {
23 if (timerIdRef.current !== null) {
24 clearInterval(timerIdRef.current);
25 }
26 };
27 }, []);
28
29 return (
30 <div className="mission-timer">
31 <p>Mission time: {seconds} seconds</p>
32 <button onClick={startTimer}>Start</button>
33 <button onClick={stopTimer}>Stop</button>
34 </div>
35 );
36}In this example, we use
useRef to store the interval ID. This is an ideal use case because:One interesting use of
useRef is tracking previous values of props or state:1function usePrevious(value) {
2 const ref = useRef();
3
4 // Update ref.current after rendering
5 useEffect(() => {
6 ref.current = value;
7 }, [value]);
8
9 // Returns the previous value (undefined during the first render)
10 return ref.current;
11}
12
13function AsteroidTracker({ asteroidId }) {
14 const [asteroidData, setAsteroidData] = useState(null);
15 // Track the previous ID
16 const previousAsteroidId = usePrevious(asteroidId);
17
18 // Effect that logs the change of the observed asteroid
19 useEffect(() => {
20 if (previousAsteroidId !== asteroidId) {
21 console.log(`Tracking changed from asteroid ${previousAsteroidId} to ${asteroidId}`);
22 // Here we could fetch new data about the asteroid
23 }
24 }, [asteroidId, previousAsteroidId]);
25
26 // Rendering...
27}The
usePrevious hook uses useRef to store the previous value. The reference is updated in useEffect, which runs after rendering, so the value stored in the ref is always one render cycle "behind" the current value.useRef can help with optimization by avoiding unnecessary effects:1function TelemetryMonitor({ shipId }) {
2 const [data, setData] = useState(null);
3 const [isConnected, setIsConnected] = useState(false);
4
5 // Store the ID to avoid unnecessary connection changes
6 const lastShipIdRef = useRef(shipId);
7
8 useEffect(() => {
9 // Check if the ship ID actually changed
10 if (lastShipIdRef.current === shipId && isConnected) {
11 console.log('Ship hasn't changed, skipping reconnection');
12 return;
13 }
14
15 // Update ref with the new ID
16 lastShipIdRef.current = shipId;
17
18 // Establish connection...
19 console.log(`Connecting to ship telemetry ${shipId}...`);
20
21 const connection = telemetryService.connect(shipId);
22 setIsConnected(true);
23
24 connection.onData(newData => {
25 setData(newData);
26 });
27
28 return () => {
29 console.log(`Disconnecting ship telemetry ${shipId}...`);
30 connection.disconnect();
31 setIsConnected(false);
32 };
33 }, [shipId]);
34
35 // Rendering...
36}The difference between
useRef and useState is crucial to understand:| Aspect | useRef | useState | |--------|--------|----------| | Powoduje re-rendering | No | Yes | | Preserves value between renders | Yes | Yes | | Accessing value | Through the
.current property | Directly by reading the state variable |
| Updating value | Direct mutation ref.current = newValue | Through updater function setState(newValue) |
| When to use? | For values that don't affect render output | For values that should trigger re-rendering |Sometimes we need greater control over when a ref is assigned. In such cases, we can use a callback ref:
1function SpectrumAnalyzer() {
2 const [frequency, setFrequency] = useState(440);
3 let canvasRef = useRef(null);
4
5 // Callback function for ref, called when ref is assigned or detached
6 const setCanvasRef = useCallback(element => {
7 // element will be null during unmounting
8 if (element) {
9 canvasRef.current = element;
10 // We can now perform initialization after the reference is assigned
11 initializeCanvas(element, frequency);
12 } else {
13 // Cleaning up resources on unmount
14 if (canvasRef.current) {
15 cleanupCanvas(canvasRef.current);
16 }
17 canvasRef.current = null;
18 }
19 }, [frequency]); // Dependency on frequency
20
21 // Function initializing canvas
22 const initializeCanvas = (canvas, freq) => {
23 console.log(`Initializing analyzer for frequency ${freq} Hz`);
24 const ctx = canvas.getContext('2d');
25 // Drawing...
26 };
27
28 return (
29 <div>
30 <canvas
31 ref={setCanvasRef}
32 width="600"
33 height="300"
34 />
35 <input
36 type="range"
37 min="20"
38 max="20000"
39 value={frequency}
40 onChange={e => setFrequency(Number(e.target.value))}
41 />
42 <div>Frequency: {frequency} Hz</div>
43 </div>
44 );
45}Callback ref gives us more control because it's called both when assigning and when removing the reference.
Sometimes we want to pass a reference to a child component. We can do this using
forwardRef:1// Child component
2const NavigationControl = forwardRef((props, ref) => {
3 return (
4 <div className="nav-control">
5 <label>{props.label}</label>
6 <input
7 ref={ref}
8 type="text"
9 className="nav-input"
10 value={props.value}
11 onChange={props.onChange}
12 />
13 </div>
14 );
15});
16
17// Parent component
18function NavigationPanel() {
19 const destinationInputRef = useRef(null);
20 const [destination, setDestination] = useState('');
21
22 const focusDestination = () => {
23 // We can access the inner input thanks to forwardRef
24 destinationInputRef.current.focus();
25 };
26
27 return (
28 <div className="nav-panel">
29 <h2>Navigation Panel</h2>
30
31 <NavigationControl
32 ref={destinationInputRef}
33 label="Destination:"
34 value={destination}
35 onChange={e => setDestination(e.target.value)}
36 />
37
38 <button onClick={focusDestination}>
39 Set destination
40 </button>
41 </div>
42 );
43}forwardRef allows a component to receive a reference from its parent and pass it to a specific element inside the component.Sometimes we need to track multiple elements, for example a list of elements:
1function StarMap({ stars }) {
2 // Storing map of references to stars
3 const starRefs = useRef({});
4
5 // Function to scroll to a specific star
6 const scrollToStar = (starId) => {
7 if (starRefs.current[starId]) {
8 starRefs.current[starId].scrollIntoView({
9 behavior: 'smooth',
10 block: 'center'
11 });
12 }
13 };
14
15 return (
16 <div className="star-map-container">
17 <div className="star-list">
18 {stars.map(star => (
19 <div
20 key={star.id}
21 // We save the reference to the element in our map
22 ref={el => starRefs.current[star.id] = el}
23 className="star-item"
24 >
25 <h3>{star.name}</h3>
26 <p>Typ: {star.type}</p>
27 <p>Distance: {star.distance} light years</p>
28 </div>
29 ))}
30 </div>
31
32 <div className="star-navigator">
33 <h3>Quick navigation</h3>
34 {stars.map(star => (
35 <button
36 key={star.id}
37 onClick={() => scrollToStar(star.id)}
38 >
39 {star.name}
40 </button>
41 ))}
42 </div>
43 </div>
44 );
45}In this example, we store a map of references where the key is the star's
id and the value is a reference to the corresponding DOM element.Since changing
ref.current does not cause a re-render, attempting to use this value directly in JSX can lead to inconsistencies:1// BAD: Changing ref.current won't cause the interface to update
2function BadCounter() {
3 const countRef = useRef(0);
4
5 const increment = () => {
6 // This change will not cause a re-render
7 countRef.current += 1;
8 console.log(`Counter: ${countRef.current}`); // Will be logged correctly
9 };
10
11 return (
12 <div>
13 <p>Counter: {countRef.current}</p> {/* Won't update after clicking */}
14 <button onClick={increment}>Increment</button>
15 </div>
16 );
17}
18
19// GOOD: Use useState for values that affect rendering
20function GoodCounter() {
21 const [count, setCount] = useState(0);
22
23 const increment = () => {
24 setCount(c => c + 1);
25 };
26
27 return (
28 <div>
29 <p>Counter: {count}</p> {/* Will update after clicking */}
30 <button onClick={increment}>Increment</button>
31 </div>
32 );
33}A common mistake is forgetting to access the value through
.current:1// BAD: Brak .current
2function MissionLog({ onAddLog }) {
3 const logInputRef = useRef(null);
4
5 const handleAddLog = () => {
6 // Error: We forgot about .current
7 const logText = logInputRef.value;
8 onAddLog(logText);
9 };
10
11 return (
12 <div>
13 <input ref={logInputRef} type="text" />
14 <button onClick={handleAddLog}>Add entry</button>
15 </div>
16 );
17}
18
19// GOOD: We use .current
20function MissionLog({ onAddLog }) {
21 const logInputRef = useRef(null);
22
23 const handleAddLog = () => {
24 // Correct usage of .current
25 const logText = logInputRef.current.value;
26 onAddLog(logText);
27 logInputRef.current.value = '';
28 };
29
30 return (
31 <div>
32 <input ref={logInputRef} type="text" />
33 <button onClick={handleAddLog}>Add entry</button>
34 </div>
35 );
36}useRef is not the appropriate tool for controlling when a component should render:1// BAD: Attempting to use ref to control rendering
2function BadRenderControl() {
3 const shouldUpdateRef = useRef(false);
4
5 const forceUpdate = () => {
6 shouldUpdateRef.current = true;
7 // Missing mechanism to force re-rendering
8 };
9
10 console.log('Rendering',shouldUpdateRef.current);
11
12 // This won't work as expected
13 if (shouldUpdateRef.current) {
14 shouldUpdateRef.current = false;
15 // Some operations...
16 }
17
18 return <button onClick={forceUpdate}>Refresh</button>;
19}
20
21// GOOD: Using useState to control rendering
22function GoodRenderControl() {
23 const [update, setUpdate] = useState(0);
24 const didUpdateRef = useRef(false);
25
26 const forceUpdate = () => {
27 setUpdate(u => u + 1); // Triggers re-rendering
28 };
29
30 console.log('Rendering',update);
31
32 useEffect(() => {
33 if (update > 0) {
34 didUpdateRef.current = true;
35 // Operations after update...
36 }
37 }, [update]);
38
39 return <button onClick={forceUpdate}>Refresh</button>;
40}Use useRef for values that don't affect rendering - if a value change should cause an interface update, use
useState.Be cautious with direct DOM manipulation - DOM manipulation through references should be limited and well thought out.
Initialize refs with appropriate initial values - for example
null for DOM references or initial values for counters.Check if ref.current exists before using it - especially for DOM references that may be
null during the first render.Use useCallback for ref-creating functions - if you're passing a reference function, consider wrapping it in
useCallback for optimization.Avoid overusing refs - although refs are useful, over-relying on them can lead to hard-to-maintain code.
Below is a more complex example that demonstrates various aspects of using
useRef:1import React, { useState, useRef, useEffect, useCallback } from 'react';
2
3function MissionVideoPlayer({ videoUrl, onProgress }) {
4 const [isPlaying, setIsPlaying] = useState(false);
5 const [duration, setDuration] = useState(0);
6 const [currentTime, setCurrentTime] = useState(0);
7 const [volume, setVolume] = useState(0.7);
8
9 // References
10 const videoRef = useRef(null); // Reference to video element
11 const progressBarRef = useRef(null); // Reference to progress bar
12 const animationFrameRef = useRef(null); // requestAnimationFrame ID for updating time
13 const isMountedRef = useRef(true); // Flag indicating whether the component is mounted
14 const lastUpdateTimeRef = useRef(0); // Time of the last update for throttling
15
16 // Function initializing video metadata
17 const handleLoadedMetadata = () => {
18 if (videoRef.current) {
19 setDuration(videoRef.current.duration);
20 videoRef.current.volume = volume;
21 }
22 };
23
24 // Function updating progress bar
25 const updateProgressBar = useCallback(() => {
26 if (!videoRef.current || !isMountedRef.current) return;
27
28 const now = Date.now();
29 if (now - lastUpdateTimeRef.current > 200) { // Throttling to 5 updates per second
30 setCurrentTime(videoRef.current.currentTime);
31 onProgress(videoRef.current.currentTime);
32 lastUpdateTimeRef.current = now;
33 }
34
35 // Continue updating if video is playing
36 if (videoRef.current.paused) return;
37 animationFrameRef.current = requestAnimationFrame(updateProgressBar);
38 }, [onProgress]);
39
40 // Start or pause playback
41 const togglePlayPause = () => {
42 if (!videoRef.current) return;
43
44 if (videoRef.current.paused) {
45 videoRef.current.play();
46 setIsPlaying(true);
47 animationFrameRef.current = requestAnimationFrame(updateProgressBar);
48 } else {
49 videoRef.current.pause();
50 setIsPlaying(false);
51 if (animationFrameRef.current) {
52 cancelAnimationFrame(animationFrameRef.current);
53 }
54 }
55 };
56
57 // Changing playback position after clicking progress bar
58 const handleProgressBarClick = (e) => {
59 if (!progressBarRef.current || !videoRef.current) return;
60
61 const rect = progressBarRef.current.getBoundingClientRect();
62 const position = (e.clientX - rect.left) / rect.width;
63 const newTime = duration * Math.max(0, Math.min(1, position));
64
65 videoRef.current.currentTime = newTime;
66 setCurrentTime(newTime);
67 };
68
69 // Changing volume
70 const handleVolumeChange = (e) => {
71 const newVolume = parseFloat(e.target.value);
72 setVolume(newVolume);
73
74 if (videoRef.current) {
75 videoRef.current.volume = newVolume;
76 }
77 };
78
79 // Effect cleaning up animationFrame and setting mounting flag
80 useEffect(() => {
81 return () => {
82 isMountedRef.current = false;
83 if (animationFrameRef.current) {
84 cancelAnimationFrame(animationFrameRef.current);
85 }
86 };
87 }, []);
88
89 // Formatting time (mm:ss)
90 const formatTime = (timeInSeconds) => {
91 const minutes = Math.floor(timeInSeconds / 60);
92 const seconds = Math.floor(timeInSeconds % 60);
93 return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
94 };
95
96 return (
97 <div className="mission-video-player">
98 <video
99 ref={videoRef}
100 src={videoUrl}
101 onLoadedMetadata={handleLoadedMetadata}
102 onEnded={() => setIsPlaying(false)}
103 />
104
105 <div className="controls">
106 <button onClick={togglePlayPause}>
107 {isPlaying ? 'Pause' : 'Play'}
108 </button>
109
110 <div className="time-display">
111 {formatTime(currentTime)} / {formatTime(duration)}
112 </div>
113
114 <div
115 ref={progressBarRef}
116 className="progress-bar"
117 onClick={handleProgressBarClick}
118 >
119 <div
120 className="progress-fill"
121 style={{ width: `${(currentTime / duration) * 100}%` }}
122 />
123 </div>
124
125 <div className="volume-control">
126 <label>Volume:</label>
127 <input
128 type="range"
129 min="0"
130 max="1"
131 step="0.05"
132 value={volume}
133 onChange={handleVolumeChange}
134 />
135 </div>
136 </div>
137 </div>
138 );
139}This example demonstrates several advanced aspects of working with
useRef:Multiple different uses of references:
videoRef, progressBarRef)animationFrameRef)isMountedRef)lastUpdateTimeRef)Integration of references with effects and callbacks
Direct manipulation of the video element through its API
Throttling state updates using time references
The
useRef hook is an essential tool in the React toolkit, particularly useful for:The key to effectively using
useRef is understanding when to use it instead of useState. The fundamental difference is that useRef does not cause a re-render when its value changes, making it the ideal choice for operations that shouldn't affect the rendering cycle.On our cosmic journey through React,
useRef is like a storage compartment on the ship - a place where you can store things you need to have on hand, but that don't require the attention of the entire crew every time they change.