Scenario for Implementation
In React, the primary interface of exchanging data among components is via Props. We can attach objects as properties (commonly known as “props”) from parent to child. The props are non-serializable, that means it can also contain functions or instances.
type Props = {
name: string,
onClick: () => void,
};
function Button(props: Props): JSX.Element {
return <button onClick={props.onClick}>{props.name}</button>;
}
Additionally, React can provide an instance of a component via ref
. When we set ref
of an HTMLElement
, the current
value of the ref
will contain the DOM instance of that element. We can also attach ref
in our custom components -
const Input = React.forwardRef((props, ref): JSX.Element => (
<input ref={ref} />
));
const inputRef = React.createRef();
<Input ref={inputRef} />;
Now, inputRef
will have an instance of input DOM Element. If we want to focus on the input programmatically, we can call inputRef.current?.focus()
.
It’s convenient in many cases when we have to update the state related to the component as a side-effect from another action. For example, we need to show alert before updating or removing an entity.
Let’s assume, we have a component for showing alert and it has the following props message
, onSuccess
, and onClose
. We can create a state that holds all the props and pass that to a common <Alert />
component.
type AlertProps = {
message: string;
onSuccess: () => void;
onClose: () => void;
}
function App() {
const [props, setProps] = React.useState<AlertProps>();
return (
<>
{props && <Alert {...props} />}
<button
onClick={() => setProps({
message: "Message 1",
onSuccess: () => alert("Action 1"),
onClose: () => setProps(undefined),
})}
>Show 1</button>
<button
onClick={() => setProps({
message: "Message 2",
onSuccess: () => alert("Action 2"),
onClose: () => setProps(undefined),
})}
>Show 2</button>
</>
);
}
The code above will work perfectly, but there is one problem - When a parent component re-renders, the child components will be re-rendered forcefully.
In the worst-case scenario, it will trigger a re-render of the parent component with very heavy child computation just for showing a modal.
That is where a modified handle of the instance can be very helpful - when the state/props changes of the component will be done programmatically most of the time. And we can do exactly that using the useImperativeHandle()
hook.
This is directly from React’s documentation -
useImperativeHandle
customizes the instance value that is exposed to parent components when usingref
. As always, imperative code using refs should be avoided in most cases. useImperativeHandle should be used withforwardRef
.
Implementation
So, let’s create an <Alert />
component with a custom instance.
// File: alert.component.tsx
import React from "react";
import Modal from "react-modal";
type AlertData = {
msg?: string;
onSuccess?: Function;
};
export type AlertInstance = {
open: (alertData: AlertData) => void;
close: () => void;
};
const customStyles = {
content: {
top: "10%",
left: "50%",
right: "auto",
bottom: "auto",
marginRight: "-50%",
transform: "translate(-50%, -50%)",
},
};
const AlertComponent = React.forwardRef<{}, {}>((props, ref) => {
const [data, setData] = React.useState<AlertData>();
const [modalIsOpen, setIsOpen] = React.useState(false);
const closeAlert = React.useCallback(() => setIsOpen(false), []);
React.useImperativeHandle(
ref,
(): AlertInstance => ({
open: (data) => {
setData(data);
setIsOpen(true);
},
close: closeAlert,
}),
[closeAlert]
);
return (
<Modal
isOpen={modalIsOpen}
style={customStyles}
contentLabel="Example Modal"
>
<b>{data?.msg ?? "Are you sure you want to continue?"}</b>
<div className="btn-row default-margin">
<button
onClick={() => {
closeAlert();
data?.onSuccess?.();
}}
>
OK
</button>
<button onClick={closeAlert}>Cancel</button>
</div>
</Modal>
);
});
export const Alert = React.memo(AlertComponent);
Here, AlertInstance
is the custom instance that will accessible in the ref
container of the <Alert />
component.
useImperativeHandle()
accepts three parameters -
- Forwarded
ref
of the component - Initialization function that will create the instance
- Dependency Array for recomputing the instance
Now, we can use the alert component in other components and access its open()
and close()
action programmatically.
import React from "react";
import { Alert, AlertInstance } from "./alert.component";
function App() {
const alertModal = (React.useRef < AlertInstance) | (null > null);
return (
<div>
<h1>
DemoApp: <b>useImperativeHandle()</b> Hook
</h1>
<button
onClick={() => {
// Open alert
alertModal.current?.open({
msg: "Alert from parent",
onSuccess: () => alert("Success"),
});
// Close alert modal after 2 seconds
setTimeout(() => alertModal.current?.close(), 2000);
}}
>
Open Alert
</button>
<Alert ref={alertModal} />
</div>
);
}
export default App;
You can find the full project here in my GitHub repository.