What is forwardRef and why is it needed?

👨‍💻 Frontend Developer 🟠 May come up 🎚️ Medium
#React

Short Answer

forwardRef — is a React API that allows parent components to pass refs through to their children. It’s needed when you want to access DOM elements or component instances of child components from parent components.

Basic usage:

// Without forwardRef - ref won't work
const Button = (props) => <button>{props.children}</button>;
 
// With forwardRef - ref passes through
const Button = React.forwardRef((props, ref) => (
  <button ref={ref}>{props.children}</button>
));

Full Answer

React forwardRef solves a specific problem: by default, function components cannot receive refs as props. ForwardRef creates a special component that can accept a ref and forward it to a DOM element or another component.

Why is forwardRef Needed?

The Problem Without forwardRef

// Parent component tries to pass ref
function Parent() {
  const buttonRef = useRef(null);
  
  return (
    // ❌ This won't work - ref will be undefined
    <FancyButton ref={buttonRef}>Click me</FancyButton>
  );
}
 
// Child component can't receive ref
function FancyButton(props) {
  // props.ref is undefined!
  return <button className="fancy">{props.children}</button>;
}

Solution with forwardRef

// Child component with forwardRef
const FancyButton = React.forwardRef((props, ref) => {
  return (
    <button ref={ref} className="fancy">
      {props.children}
    </button>
  );
});
 
// Parent component
function Parent() {
  const buttonRef = useRef(null);
  
  const handleClick = () => {
    // ✅ Now we can access the button
    buttonRef.current.focus();
  };
  
  return (
    <>
      <FancyButton ref={buttonRef}>Click me</FancyButton>
      <button onClick={handleClick}>Focus fancy button</button>
    </>
  );
}

Common Use Cases

1. Accessing DOM Elements in Child Components

// Custom input component
const CustomInput = React.forwardRef((props, ref) => {
  return (
    <div className="input-wrapper">
      <label>{props.label}</label>
      <input ref={ref} {...props} />
    </div>
  );
});
 
// Usage in form
function LoginForm() {
  const usernameRef = useRef(null);
  const passwordRef = useRef(null);
  
  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(usernameRef.current.value);
    console.log(passwordRef.current.value);
  };
  
  useEffect(() => {
    // Auto-focus on mount
    usernameRef.current.focus();
  }, []);
  
  return (
    <form onSubmit={handleSubmit}>
      <CustomInput ref={usernameRef} label="Username" />
      <CustomInput ref={passwordRef} label="Password" type="password" />
      <button type="submit">Login</button>
    </form>
  );
}

2. Integration with Third-Party Libraries

// Wrapper for third-party component
const MapWrapper = React.forwardRef((props, ref) => {
  const mapContainerRef = useRef(null);
  
  useImperativeHandle(ref, () => ({
    // Expose custom methods
    centerMap: (lat, lng) => {
      if (mapContainerRef.current) {
        // Call third-party library method
        mapLibrary.setCenter(mapContainerRef.current, { lat, lng });
      }
    },
    getZoom: () => {
      return mapLibrary.getZoom(mapContainerRef.current);
    }
  }));
  
  useEffect(() => {
    // Initialize third-party library
    mapLibrary.init(mapContainerRef.current, props.options);
  }, []);
  
  return <div ref={mapContainerRef} className="map-container" />;
});
 
// Parent component
function App() {
  const mapRef = useRef(null);
  
  const handleCenterMap = () => {
    mapRef.current.centerMap(40.7128, -74.0060); // New York
  };
  
  return (
    <div>
      <MapWrapper ref={mapRef} options={{ zoom: 10 }} />
      <button onClick={handleCenterMap}>Center on New York</button>
    </div>
  );
}

3. Creating Reusable UI Components

// Reusable modal component
const Modal = React.forwardRef((props, ref) => {
  const [isOpen, setIsOpen] = useState(false);
  const modalRef = useRef(null);
  
  useImperativeHandle(ref, () => ({
    open: () => setIsOpen(true),
    close: () => setIsOpen(false),
    toggle: () => setIsOpen(prev => !prev)
  }));
  
  useEffect(() => {
    if (isOpen && modalRef.current) {
      modalRef.current.focus();
    }
  }, [isOpen]);
  
  if (!isOpen) return null;
  
  return (
    <div className="modal-overlay" onClick={() => setIsOpen(false)}>
      <div 
        ref={modalRef}
        className="modal-content"
        onClick={e => e.stopPropagation()}
        tabIndex={-1}
      >
        <button 
          className="modal-close" 
          onClick={() => setIsOpen(false)}
        >
          ×
        </button>
        {props.children}
      </div>
    </div>
  );
});
 
// Usage
function App() {
  const modalRef = useRef(null);
  
  return (
    <div>
      <button onClick={() => modalRef.current.open()}>
        Open Modal
      </button>
      
      <Modal ref={modalRef}>
        <h2>Modal Content</h2>
        <p>This is a reusable modal component</p>
      </Modal>
    </div>
  );
}

Advanced Patterns with forwardRef

1. Multiple Refs Handling

// Component that needs both internal and forwarded ref
const VideoPlayer = React.forwardRef((props, forwardedRef) => {
  const internalRef = useRef(null);
  
  // Combine refs
  const setRefs = useCallback((node) => {
    // Set internal ref
    internalRef.current = node;
    
    // Set forwarded ref
    if (forwardedRef) {
      if (typeof forwardedRef === 'function') {
        forwardedRef(node);
      } else {
        forwardedRef.current = node;
      }
    }
  }, [forwardedRef]);
  
  useEffect(() => {
    // Use internal ref for component logic
    if (internalRef.current) {
      internalRef.current.volume = 0.5;
    }
  }, []);
  
  return <video ref={setRefs} {...props} />;
});

2. Conditional Ref Forwarding

// Forward ref to different elements based on props
const FlexibleInput = React.forwardRef((props, ref) => {
  if (props.multiline) {
    return <textarea ref={ref} {...props} />;
  }
  
  if (props.type === 'select') {
    return (
      <select ref={ref} {...props}>
        {props.options.map(opt => (
          <option key={opt.value} value={opt.value}>
            {opt.label}
          </option>
        ))}
      </select>
    );
  }
  
  return <input ref={ref} {...props} />;
});

3. HOC with forwardRef

// Higher-order component that preserves ref
function withTooltip(Component) {
  const WithTooltipComponent = React.forwardRef((props, ref) => {
    const [showTooltip, setShowTooltip] = useState(false);
    
    return (
      <div className="tooltip-wrapper">
        <Component
          {...props}
          ref={ref}
          onMouseEnter={() => setShowTooltip(true)}
          onMouseLeave={() => setShowTooltip(false)}
        />
        {showTooltip && (
          <div className="tooltip">{props.tooltip}</div>
        )}
      </div>
    );
  });
  
  // Set display name for debugging
  WithTooltipComponent.displayName = 
    `withTooltip(${Component.displayName || Component.name})`;
  
  return WithTooltipComponent;
}
 
// Usage
const ButtonWithTooltip = withTooltip(
  React.forwardRef((props, ref) => (
    <button ref={ref} {...props} />
  ))
);

TypeScript with forwardRef

Basic TypeScript Usage

interface ButtonProps {
  variant?: 'primary' | 'secondary';
  size?: 'small' | 'medium' | 'large';
  children: React.ReactNode;
  onClick?: () => void;
}
 
// Type-safe forwardRef component
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  (props, ref) => {
    const { variant = 'primary', size = 'medium', ...rest } = props;
    
    return (
      <button
        ref={ref}
        className={`btn btn-${variant} btn-${size}`}
        {...rest}
      />
    );
  }
);
 
// Usage with TypeScript
function App() {
  const buttonRef = useRef<HTMLButtonElement>(null);
  
  const focusButton = () => {
    buttonRef.current?.focus();
  };
  
  return (
    <Button ref={buttonRef} variant="primary">
      Click me
    </Button>
  );
}

Complex TypeScript Example

// Custom ref handle interface
interface VideoPlayerHandle {
  play: () => void;
  pause: () => void;
  seekTo: (time: number) => void;
}
 
interface VideoPlayerProps {
  src: string;
  poster?: string;
  autoPlay?: boolean;
}
 
const VideoPlayer = React.forwardRef<VideoPlayerHandle, VideoPlayerProps>(
  (props, ref) => {
    const videoRef = useRef<HTMLVideoElement>(null);
    
    useImperativeHandle(ref, () => ({
      play: () => {
        videoRef.current?.play();
      },
      pause: () => {
        videoRef.current?.pause();
      },
      seekTo: (time: number) => {
        if (videoRef.current) {
          videoRef.current.currentTime = time;
        }
      }
    }), []);
    
    return (
      <video
        ref={videoRef}
        src={props.src}
        poster={props.poster}
        autoPlay={props.autoPlay}
      />
    );
  }
);
 
// Type-safe usage
function App() {
  const playerRef = useRef<VideoPlayerHandle>(null);
  
  return (
    <div>
      <VideoPlayer ref={playerRef} src="video.mp4" />
      <button onClick={() => playerRef.current?.play()}>Play</button>
      <button onClick={() => playerRef.current?.pause()}>Pause</button>
      <button onClick={() => playerRef.current?.seekTo(30)}>
        Skip to 30s
      </button>
    </div>
  );
}

Performance Considerations

Memo with forwardRef

// Optimized component with memo and forwardRef
const ExpensiveComponent = React.memo(
  React.forwardRef((props, ref) => {
    console.log('ExpensiveComponent rendered');
    
    // Heavy computations
    const result = useMemo(() => {
      return heavyCalculation(props.data);
    }, [props.data]);
    
    return (
      <div ref={ref}>
        {result}
      </div>
    );
  })
);
 
// Custom comparison function
const OptimizedComponent = React.memo(
  React.forwardRef((props, ref) => {
    return <div ref={ref}>{props.content}</div>;
  }),
  (prevProps, nextProps) => {
    // Custom comparison logic
    return prevProps.content === nextProps.content;
  }
);

Common Pitfalls and Solutions

1. Forgetting Display Name

// ❌ Bad - no display name
const MyComponent = React.forwardRef((props, ref) => {
  return <div ref={ref} />;
});
 
// ✅ Good - with display name
const MyComponent = React.forwardRef((props, ref) => {
  return <div ref={ref} />;
});
MyComponent.displayName = 'MyComponent';

2. Incorrect Ref Type

// ❌ Wrong - ref points to React component, not DOM
const Card = React.forwardRef((props, ref) => {
  return <AnotherComponent ref={ref} />;
});
 
// ✅ Correct - ref points to DOM element
const Card = React.forwardRef((props, ref) => {
  return (
    <div ref={ref} className="card">
      <AnotherComponent />
    </div>
  );
});

3. Ref Callback vs Ref Object

const FlexibleComponent = React.forwardRef((props, ref) => {
  const setRef = (node) => {
    // Handle both ref types
    if (ref) {
      if (typeof ref === 'function') {
        ref(node);
      } else {
        ref.current = node;
      }
    }
  };
  
  return <div ref={setRef}>{props.children}</div>;
});

Best Practices

  1. Use meaningful display names — helps with debugging
  2. Document imperative handles — clearly describe exposed methods
  3. Type your refs in TypeScript — prevents runtime errors
  4. Combine with memo wisely — optimize performance when needed
  5. Keep forwarded refs simple — avoid complex ref logic
  6. Test ref functionality — ensure refs work as expected

ForwardRef is an essential tool for building reusable React components that need to expose their internal DOM elements or imperative APIs to parent components.


Want more interview preparation articles? Subscribe to EasyAdvice, bookmark the site and improve every day 💪