We use cookies to enhance your experience on the site
CodeWorlds

References with useRef

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.

What is useRef?

The

useRef
hook is one of the built-in hooks in React that:

  1. Creates a mutable reference field that survives throughout the entire component lifecycle
  2. Does not cause component re-rendering when its value changes
  3. Can be used to store references to DOM elements or any other values

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.

Main use cases for useRef

1. Accessing DOM elements

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:

  1. We create a reference
    thrusterControlRef
    using
    useRef(null)
  2. We assign this reference to the
    input
    element using the
    ref
    attribute
  3. We use the reference to call the
    focus()
    method on the DOM element

2. Storing mutable values between renders

useRef
can also be used to store values that:

  • Should not cause a re-render when changed
  • Must remain stable throughout the component's lifecycle
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:

  • We don't need a re-render when the interval ID changes
  • We need to preserve the ID between renders to be able to stop the interval
  • This value is not used directly in the rendered JSX

3. Tracking previous values

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.

4. Avoiding unnecessary effects

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}

useRef vs useState - when to use which?

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 |

When to use useRef:

  • Accessing DOM elements
  • Storing timer and interval IDs
  • Storing previous values
  • Values that change frequently but don't affect UI
  • Storing class instances and objects that shouldn't be recreated on every render

When to use useState:

  • Data that affects what the user sees
  • Values whose changes should cause UI updates
  • State that controls the behavior or appearance of the user interface

Advanced useRef patterns

1. Callback Refs

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.

2. Combining useRef with forwardRef

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.

3. Storing multiple references

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.

Common pitfalls when using useRef

1. Attempting to render directly from ref.current

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}

2. Forgetting about .current

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}

3. Improper use of ref for render control

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}

Best practices with useRef

  1. Use useRef for values that don't affect rendering - if a value change should cause an interface update, use

    useState
    .

  2. Be cautious with direct DOM manipulation - DOM manipulation through references should be limited and well thought out.

  3. Initialize refs with appropriate initial values - for example

    null
    for DOM references or initial values for counters.

  4. Check if ref.current exists before using it - especially for DOM references that may be

    null
    during the first render.

  5. Use useCallback for ref-creating functions - if you're passing a reference function, consider wrapping it in

    useCallback
    for optimization.

  6. Avoid overusing refs - although refs are useful, over-relying on them can lead to hard-to-maintain code.

Practical example: Advanced mission video player

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
:

  1. Multiple different uses of references:

    • Access to DOM element (
      videoRef
      ,
      progressBarRef
      )
    • Storing animation ID (
      animationFrameRef
      )
    • Mounting flag (
      isMountedRef
      )
    • Tracking values for throttling (
      lastUpdateTimeRef
      )
  2. Integration of references with effects and callbacks

  3. Direct manipulation of the video element through its API

  4. Throttling state updates using time references

Summary

The

useRef
hook is an essential tool in the React toolkit, particularly useful for:

  • Storing references to DOM elements
  • Maintaining values between renders without triggering re-rendering
  • Tracking previous values
  • Storing timer, interval, and requestAnimationFrame IDs
  • Storing flags and values that don't directly affect the interface

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.

Go to CodeWorlds