HomeAbout Me

Advanced React APIs 7: Imperative Handles

By Daniel Nguyen
Published in React JS
May 27, 2025
1 min read
Advanced React APIs 7: Imperative Handles

Imperative Handles

Sometimes you need to expose a method to the parent component that allows the parent to imperatively interact with the child component. This is done using a ref which you get from the useRef hook. You’ll recall that the useRef hook allows you to have an object that’s associated to a particular instance of a component which persists across renders and doesn’t trigger rerenders when it’s changed.

So a parent component can pass a ref to a child component and then the child component can attach methods to that ref which the parent can then call:

type InputAPI = { focusInput: () => void }
function MyInput({
ref,
...props
}: React.InputHTMLAttributes<HTMLInputElement> & {
ref: React.RefObject<InputAPI>
}) {
const inputRef = useRef()
ref.current = {
focusInput: () => inputRef.current.focus(),
}
return <input ref={inputRef} {...props} />
}
function App() {
const myInputRef = useRef<InputAPI>(null)
return (
<div>
<MyInput ref={myInputRef} placeholder="Enter your name" />
<button onClick={() => myInputRef.current.focusInput()}>
Focus the input
</button>
</div>
)
}

This actually works, however there are some edge case bugs with this approach when applied in React’s concurrent/suspense features (also it doesn’t support callback refs). So instead, we’ll use the useImperativeHandle hook to do this:

type InputAPI = { focusInput: () => void }
function MyInput({
ref,
...props
}: React.InputHTMLAttributes<HTMLInputElement> & {
ref: React.RefObject<InputAPI>
}) {
const inputRef = useRef()
useImperativeHandle(
ref,
() => ({ focusInput: () => inputRef.current.focus() }),
[],
)
return <input ref={inputRef} {...props} />
}

You’ll notice that empty array. That’s another dependency array. We don’t include the inputRef in there even though it’s used in the function because you actually don’t need to include refs in the dependency array. Learn more about this in Why you shouldn’t put refs in a dependency array.

useImperativeHandle allows us to expose imperative methods to developers who pass a ref prop to our component which can be useful when you have something that needs to happen and is hard to deal with declaratively.

NOTE: most of the time you should not need useImperativeHandle. Before you reach for it, really ask yourself whether there’s any other way to accomplish what you’re trying to do. Imperative code can sometimes be really hard to follow and it’s much better to make your APIs declarative if possible. For more on this, read Imperative vs Declarative Programming

useImperativeHandle

👨‍💼 We’ve got a new thing for you to work on.

🧝‍♂️ I’ve put together a Scrollable component which is a wrapper around a div that has a way to scroll to the top and bottom of the content. We want to be able to add buttons to the App that will allow users to scroll to the top and bottom of the content when clicked.

👨‍💼 So we need you to useImperativeHandle to expose a scrollToTop and scrollToBottom method from the Scrollable component. These methods are already implemented, you just need to expose them.

index.css

.messaging-app {
max-width: 350px;
margin: auto;
}
.messaging-app [role='log'] {
margin: auto;
height: 300px;
overflow-y: scroll;
width: 300px;
outline: 1px solid black;
padding: 30px 10px;
}
.messaging-app [role='log'] hr {
margin-top: 8px;
margin-bottom: 8px;
}

index.tsx

import { useImperativeHandle, useLayoutEffect, useRef, useState } from 'react'
import * as ReactDOM from 'react-dom/client'
import { allMessages } from './messages'
type ScrollableImperativeAPI = {
scrollToTop: () => void
scrollToBottom: () => void
}
function Scrollable({
children,
scrollableRef,
}: { children: React.ReactNode } & {
scrollableRef: React.RefObject<ScrollableImperativeAPI | null>
}) {
const containerRef = useRef<HTMLDivElement>(null)
useLayoutEffect(() => {
scrollToBottom()
})
function scrollToTop() {
if (!containerRef.current) return
containerRef.current.scrollTop = 0
}
function scrollToBottom() {
if (!containerRef.current) return
containerRef.current.scrollTop = containerRef.current.scrollHeight
}
useImperativeHandle(scrollableRef, () => ({
scrollToTop,
scrollToBottom,
}))
return (
<div ref={containerRef} role="log">
{children}
</div>
)
}
function App() {
const scrollableRef = useRef<ScrollableImperativeAPI>(null)
const [messages, setMessages] = useState(allMessages.slice(0, 8))
function addMessage() {
if (messages.length < allMessages.length) {
setMessages(allMessages.slice(0, messages.length + 1))
}
}
function removeMessage() {
if (messages.length > 0) {
setMessages(allMessages.slice(0, messages.length - 1))
}
}
const scrollToTop = () => scrollableRef.current?.scrollToTop()
const scrollToBottom = () => scrollableRef.current?.scrollToBottom()
return (
<div className="messaging-app">
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<button onClick={addMessage}>add message</button>
<button onClick={removeMessage}>remove message</button>
</div>
<hr />
<div>
<button onClick={scrollToTop}>scroll to top</button>
</div>
<Scrollable scrollableRef={scrollableRef}>
{messages.map((message, index, array) => (
<div key={message.id}>
<strong>{message.author}</strong>: <span>{message.content}</span>
{array.length - 1 === index ? null : <hr />}
</div>
))}
</Scrollable>
<div>
<button onClick={scrollToBottom}>scroll to bottom</button>
</div>
</div>
)
}
const rootEl = document.createElement('div')
document.body.append(rootEl)
ReactDOM.createRoot(rootEl).render(<App />)
/*
eslint
@typescript-eslint/no-unused-vars: "off",
*/

messages.tsx

export type Message = { id: string; author: string; content: string }
export const allMessages: Array<Message> = [
`Leia: Aren't you a little short to be a stormtrooper?`,
`Luke: What? Oh... the uniform. I'm Luke Skywalker. I'm here to rescue you.`,
`Leia: You're who?`,
`Luke: I'm here to rescue you. I've got your R2 unit. I'm here with Ben Kenobi.`,
`Leia: Ben Kenobi is here! Where is he?`,
`Luke: Come on!`,
`Luke: Will you forget it? I already tried it. It's magnetically sealed!`,
`Leia: Put that thing away! You're going to get us all killed.`,
`Han: Absolutely, Your Worship. Look, I had everything under control until you led us down here. You know, it's not going to take them long to figure out what happened to us.`,
`Leia: It could be worse...`,
`Han: It's worse.`,
`Luke: There's something alive in here!`,
`Han: That's your imagination.`,
`Luke: Something just moves past my leg! Look! Did you see that?`,
`Han: What?`,
`Luke: Help!`,
`Han: Luke! Luke! Luke!`,
`Leia: Luke!`,
`Leia: Luke, Luke, grab a hold of this.`,
`Luke: Blast it, will you! My gun's jammed.`,
`Han: Where?`,
`Luke: Anywhere! Oh!!`,
`Han: Luke! Luke!`,
`Leia: Grab him!`,
`Leia: What happened?`,
`Luke: I don't know, it just let go of me and disappeared...`,
`Han: I've got a very bad feeling about this.`,
`Luke: The walls are moving!`,
`Leia: Don't just stand there. Try to brace it with something.`,
`Luke: Wait a minute!`,
`Luke: Threepio! Come in Threepio! Threepio! Where could he be?`,
].map((m, i) => ({
id: String(i),
author: m.split(': ')[0]!,
content: m.split(': ')[1]!,
}))

Tags

#React

Share

Previous Article
Advanced React APIs 6: Layout Computation

Related Posts

React Testing 8: Testing custom hook
September 09, 2025
1 min
© 2025, All Rights Reserved.
Powered By

Quick Links

About Me

Legal Stuff

Social Media