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 ProgramminguseImperativeHandle
👨💼 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.
.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;}
import { useImperativeHandle, useLayoutEffect, useRef, useState } from 'react'import * as ReactDOM from 'react-dom/client'import { allMessages } from './messages'type ScrollableImperativeAPI = {scrollToTop: () => voidscrollToBottom: () => 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) returncontainerRef.current.scrollTop = 0}function scrollToBottom() {if (!containerRef.current) returncontainerRef.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",*/
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]!,}))
Quick Links
Legal Stuff
Social Media