useImperativeHandle: A New Perspective in Making React Components
Jun 8, 2022
useImperativeHandle
hook is an unusual hook in React, to an extent that it is discouraged to use in the React’s official documentation. React is a declarative framework and as the hook’s name suggests, it is giving a break from React’s declarative style by letting parent component call functions within the child component.
This is not something new to React, we have the useRef
/createRef
to access a DOM Elements API and call their functions. But in the case of a child component, you can’t access functions within it and call them to make changes in it using useRef
.
Let’s look at some code.
const App = () => {
const [isVisible, setVisible] = useState(false);
const showDialog = () => setVisible(true);
return (
<>
<button onClick={showDialog}>
Show Dialog
</button>
<Dialog isVisible={isVisible}>
Content within dialog
</Dialog>
</>
)
}
This is the declarative way of implementing a dialog component. You have to pass visibility as a property to the component. This is the default way in which we all work.
Let’s look at an alternative but imperative way.
const App = () => {
const dialogRef = useRef();
return (
<>
<button onClick={() => dialogRef.current.show()}>
Show Dialog
</button>
<Dialog isVisible={isVisible}>
Content within dialog
</Dialog>
</>
)
}
Here we are accessing the component as if it’s an object and call functions on it. State of the component is maitained within itself. We don’t see such code often in React projects but it’s certainly possible using useImperativeHandle
.
Following is the implementation of such a Dialog
component.
const Dialog = React.forwardRef((props, ref) => {
const [visible, setVisible] = useState(false);
useImperativeHandle(ref, () => {
return {
show() {
setVisible(true);
},
hide() {
setVisible(false);
},
toggle() {
setVisible(v => !v);
}
}
} );
return (
<div class="dialog-style">
This is a dialog box
</div>
);
});
We have to use forwardRef
to take the ref from parent and assign it to the object that is returned from the second argument of useImperativeHandle
. That’s it. Now we have exposed our component with a custom API to the parent component.
State of the dialog’s visibility is kept within the component and this relieves us from defining the state everytime we have to use this component. But this doesn’t mean we should use useImperativeHandle
more often.
I chose Dialog component as an example, because showing dialogs is one of the cases which happen mostly on an event like button click. So imperative API may be easier to consume in this case. But React is designed as a declarative framework and popular packages like Redux maintain the state and the UI is just a representation of state maintained somewhere else. So for most cases, if you use imperative API, you have to use a lot of useEffect
s to trigger these imperative APIs which is not good.
There are some cases where it suits better. Whenever you want expose functions from more than one DOM Element to a parent component, you can go for useImperativeHandle
.
Imagine that you build a credit card input component where you have four input boxes and at times first or last input has to be focussed. In such a case, useImperativeHandle
is a good choice. Check out the code below.
const CreditCardInput = React.forwardRef((props, ref) => {
const firstInputRef = useRef();
const lastInputRef = useRef();
useImperativeHandle(ref, () => {
return {
focusFirst() {
firstInputRef.current.focus()
},
focusSecond() {
secondInputRef.current.focus()
},
}
});
return (
<div class="style">
<input type="number" ref={firstRef} />
<input type="number" />
<input type="number" />
<input type="number" ref={lasttRef} />
</div>
);
});
useImperativeHandle
is a nice tool which has to be rarely and cautiously used. Otherwise you could easily end up inventing Angular in React. Think about the use case you have in hand before coming to a conclusion.