React forwardRef TypeScript: Typing Refs in Components
By default, React components cannot receive refs. forwardRef is React's solution: it wraps a component and allows parent components to pass a ref that reaches into the component's internal DOM element or handle. When typed correctly with TypeScript, forwardRef ensures the ref type matches what the component exposes.
In teams I've worked with, untyped forwardRef created confusion about what a ref could do. TypeScript eliminates that by making the ref's capabilities explicit in the type signature.
The Basics of forwardRef
A standard component cannot be passed a ref prop—React ignores it. forwardRef fixes this by returning a component that accepts a ref parameter:
interface ButtonProps {
label: string;
onClick: () => void;
}
const StyledButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ label, onClick }, ref) => (
<button ref={ref} onClick={onClick}>
{label}
</button>
)
);
// Usage: parent can now attach a ref
function App() {
const buttonRef = React.useRef<HTMLButtonElement>(null);
const focusButton = () => {
buttonRef.current?.focus();
};
return (
<>
<StyledButton ref={buttonRef} label="Click me" onClick={focusButton} />
<button onClick={focusButton}>Focus the styled button</button>
</>
);
}
The forwardRef<RefType, PropsType> generic takes two type parameters: the type of the ref being forwarded (e.g., HTMLButtonElement), and the props type. TypeScript validates that the ref passed by the parent matches the declared ref type.
Typing Custom Ref Handles with useImperativeHandle
Sometimes you don't want to expose a DOM element directly. Instead, define a custom interface describing the methods and values the ref can access:
interface CustomInputHandle {
focus: () => void;
clear: () => void;
getValue: () => string;
}
interface CustomInputProps {
defaultValue?: string;
placeholder?: string;
}
const CustomInput = React.forwardRef<CustomInputHandle, CustomInputProps>(
({ defaultValue, placeholder }, ref) => {
const inputRef = React.useRef<HTMLInputElement>(null);
React.useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current?.focus();
},
clear: () => {
if (inputRef.current) {
inputRef.current.value = '';
}
},
getValue: () => {
return inputRef.current?.value ?? '';
},
}));
return (
<input
ref={inputRef}
defaultValue={defaultValue}
placeholder={placeholder}
/>
);
}
);
// Usage
function Form() {
const inputRef = React.useRef<CustomInputHandle>(null);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log('Value:', inputRef.current?.getValue());
inputRef.current?.clear();
};
return (
<form onSubmit={handleSubmit}>
<CustomInput ref={inputRef} placeholder="Enter text" />
<button type="submit">Submit</button>
</form>
);
}
useImperativeHandle allows you to define a public API for the ref. The first argument is the ref itself, and the second is a callback that returns an object matching the handle interface. TypeScript validates that the returned object implements CustomInputHandle completely.
This pattern is powerful because it decouples the component's internal DOM structure from its public API. You can refactor the internal implementation without breaking code that uses the ref.
Generic forwardRef Components
For reusable component libraries, make forwardRef generic so callers can specify the element and props type:
interface PolymorphicProps<C extends React.ElementType> {
as?: C;
children: React.ReactNode;
}
const Polymorphic = React.forwardRef<HTMLElement, PolymorphicProps<'div'>>(
({ as: Component = 'div', children }, ref) => (
<Component ref={ref} style={{ padding: '1rem', border: '1px solid #ccc' }}>
{children}
</Component>
)
);
For more complex polymorphic components, use a helper type:
type PolymorphicComponentProps<
C extends React.ElementType,
Props = {}
> = Props & React.ComponentPropsWithoutRef<C> & {
as?: C;
};
type PolymorphicComponent = <C extends React.ElementType = 'div'>(
props: PolymorphicComponentProps<C>
) => React.ReactElement | null;
const Box: PolymorphicComponent = React.forwardRef(
({ as: Component = 'div', ...props }, ref) => (
<Component ref={ref} {...props} />
)
);
// Usage: Box can be a div, section, article, or custom component
<Box as="section" ref={sectionRef} className="container" />;
<Box as="article" ref={articleRef} />;
This pattern allows the same component to render different elements while preserving type safety for each variant.
Combining forwardRef with TypeScript Generics
For components that need flexible generic types for their props:
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T, index: number) => string | number;
}
interface ListHandle {
scrollToIndex: (index: number) => void;
}
const List = React.forwardRef<ListHandle, ListProps<any>>(
function ListComponent<T>({ items, renderItem, keyExtractor }, ref) {
const containerRef = React.useRef<HTMLDivElement>(null);
React.useImperativeHandle(ref, () => ({
scrollToIndex: (index: number) => {
const element = containerRef.current?.children[index] as HTMLElement;
element?.scrollIntoView({ behavior: 'smooth' });
},
}));
return (
<div ref={containerRef}>
{items.map((item, index) => (
<div key={keyExtractor(item, index)}>
{renderItem(item, index)}
</div>
))}
</div>
);
}
) as <T,>(
props: ListProps<T> & { ref?: React.ForwardedRef<ListHandle> }
) => React.ReactElement;
// Usage
interface User {
id: number;
name: string;
}
function UserList() {
const listRef = React.useRef<ListHandle>(null);
const users: User[] = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
];
return (
<>
<List<User>
ref={listRef}
items={users}
renderItem={(user) => <div>{user.name}</div>}
keyExtractor={(user) => user.id}
/>
<button onClick={() => listRef.current?.scrollToIndex(0)}>
Scroll to top
</button>
</>
);
}
This approach allows forwardRef components to work with generic props while remaining fully typed.
forwardRef with Optional Refs
Sometimes a component should work with or without a ref. Use React.ComponentPropsWithRef to include the ref in the props type:
type ButtonProps = React.ComponentPropsWithRef<'button'> & {
variant?: 'primary' | 'secondary';
};
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = 'primary', ...rest }, ref) => (
<button
ref={ref}
{...rest}
className={`btn btn-${variant}`}
/>
)
);
This pattern ensures the component works both with and without a ref, and all native button props are available to callers.
Key Takeaways
forwardRef<RefType, PropsType>takes two generics: the ref type and the props type.- Use
useImperativeHandleto define a custom public API for the ref, decoupling the internal implementation. - For polymorphic components, use
React.ElementTypeandComponentPropsWithoutRefto handle multiple element types. - Generic
forwardRefcomponents require special casing to preserve type inference for callers. - Always validate that the object returned by
useImperativeHandlematches the declared handle type.
Frequently Asked Questions
Why do I need forwardRef instead of just passing a ref prop?
React by default does not forward refs to function components because it would be ambiguous which element receives the ref. forwardRef makes it explicit which element or handle the parent accesses.
Can I use forwardRef with class components?
No. Class components don't need forwardRef—you can attach a ref directly. forwardRef is for function components.
What is the difference between useImperativeHandle and a direct ref?
Direct refs expose the DOM element or component's internal state. useImperativeHandle lets you expose only the methods you choose, hiding implementation details. This is better for libraries and shared components.
Can I forward multiple refs from a component?
Not directly—a component can only have one ref parameter. If you need multiple, pass them as props or return an object with multiple ref fields using useImperativeHandle.