We use cookies to enhance your experience on the site
CodeWorlds

React 19 - New Way of Passing ref (Without forwardRef)

In React 19, a long-awaited change has arrived -- passing

ref
to function components has become much simpler! Just as new communication systems on our spaceships eliminate the need for complicated relays, React 19 eliminates the need to use
forwardRef
.

The Old Way with forwardRef (React 18 and Earlier)

In previous React versions, to pass a

ref
to a child component, we had to use
forwardRef
:

1import { forwardRef, useRef } from 'react';
2
3// Old way - requires forwardRef
4const SpaceshipButton = forwardRef((props, ref) => {
5  return (
6    <button ref={ref} className="spaceship-btn" {...props}>
7      {props.children}
8    </button>
9  );
10});
11
12// Usage
13function MissionControl() {
14  const buttonRef = useRef(null);
15
16  const focusButton = () => {
17    buttonRef.current?.focus();
18  };
19
20  return (
21    <div>
22      <SpaceshipButton ref={buttonRef}>
23        Start engines
24      </SpaceshipButton>
25      <button onClick={focusButton}>Focus on button</button>
26    </div>
27  );
28}

The New Way in React 19 - ref as a Prop

In React 19,

ref
is simply a regular prop! There is no need to use
forwardRef
:

1import { useRef } from 'react';
2
3// New way in React 19 - ref is just a prop!
4function SpaceshipButton({ ref, children, ...props }) {
5  return (
6    <button ref={ref} className="spaceship-btn" {...props}>
7      {children}
8    </button>
9  );
10}
11
12// Usage - exactly the same!
13function MissionControl() {
14  const buttonRef = useRef(null);
15
16  const focusButton = () => {
17    buttonRef.current?.focus();
18  };
19
20  return (
21    <div>
22      <SpaceshipButton ref={buttonRef}>
23        Start engines
24      </SpaceshipButton>
25      <button onClick={focusButton}>Focus on button</button>
26    </div>
27  );
28}

Practical Examples

1. Input with Auto-Focus

1// React 19 - simple input component
2function CoordinateInput({ ref, label, ...props }) {
3  return (
4    <div className="coordinate-field">
5      <label>{label}</label>
6      <input ref={ref} {...props} />
7    </div>
8  );
9}
10
11function NavigationPanel() {
12  const latitudeRef = useRef(null);
13  const longitudeRef = useRef(null);
14
15  useEffect(() => {
16    // Auto-focus on first input
17    latitudeRef.current?.focus();
18  }, []);
19
20  const handleLatitudeEnter = (e) => {
21    if (e.key === 'Enter') {
22      longitudeRef.current?.focus();
23    }
24  };
25
26  return (
27    <form className="nav-panel">
28      <h2>Enter destination coordinates</h2>
29
30      <CoordinateInput
31        ref={latitudeRef}
32        label="Galactic latitude"
33        onKeyDown={handleLatitudeEnter}
34      />
35
36      <CoordinateInput
37        ref={longitudeRef}
38        label="Galactic longitude"
39      />
40
41      <button type="submit">Set course</button>
42    </form>
43  );
44}

useImperativeHandle - Still Useful

useImperativeHandle
still works, but now without
forwardRef
:

1import { useImperativeHandle, useRef } from 'react';
2
3function VideoPlayer({ ref, src }) {
4  const videoRef = useRef(null);
5
6  // Expose only selected methods
7  useImperativeHandle(ref, () => ({
8    play: () => videoRef.current?.play(),
9    pause: () => videoRef.current?.pause(),
10    seek: (time) => {
11      if (videoRef.current) {
12        videoRef.current.currentTime = time;
13      }
14    },
15    getCurrentTime: () => videoRef.current?.currentTime || 0,
16  }), []);
17
18  return (
19    <video ref={videoRef} src={src} className="space-video">
20      Your browser does not support video.
21    </video>
22  );
23}
24
25function MediaController() {
26  const playerRef = useRef(null);
27
28  return (
29    <div>
30      <VideoPlayer ref={playerRef} src="/space-documentary.mp4" />
31
32      <div className="controls">
33        <button onClick={() => playerRef.current?.play()}>
34          Play
35        </button>
36        <button onClick={() => playerRef.current?.pause()}>
37          Pause
38        </button>
39        <button onClick={() => playerRef.current?.seek(0)}>
40          From start
41        </button>
42      </div>
43    </div>
44  );
45}

Migration from forwardRef

If you have existing code with

forwardRef
, migration is straightforward:

1// Before (React 18)
2const OldComponent = forwardRef(function OldComponent(props, ref) {
3  return <div ref={ref}>{props.children}</div>;
4});
5
6// After (React 19)
7function NewComponent({ ref, children }) {
8  return <div ref={ref}>{children}</div>;
9}
10
11// Usage remains identical!
12<OldComponent ref={myRef}>Old</OldComponent>
13<NewComponent ref={myRef}>New</NewComponent>

Summary

React 19 introduces a significant simplification in ref forwarding:

  1. ref is a regular prop - does not require special treatment
  2. forwardRef is deprecated - still works, but migration is recommended
  3. useImperativeHandle works without forwardRef - pass ref as a prop
  4. Callback refs unchanged - work the same as before
  5. TypeScript - ref now has a type in props, no special handling required

This simplification makes components more predictable and easier to understand -- like modern space navigation systems that eliminate unnecessary complications!

Go to CodeWorlds