React Hooks review #8: useImperativeHandle
The goal of this series is simple: summarize all the eleven official React hooks. But before writing about the last three allow me to quote:
Allow me to preface this answer by stating that all of these hooks are very rarely used. 99% of the time, you won’t need these. They are only meant to cover some rare corner-case scenarios.
When to use useImperativeHandle, useLayoutEffect, and useDebugValue
- The three basic Hooks:
useState
,useEffect
,useContext
. - The seven additional Hooks:
useReducer
,useCallback
,useMemo
,useRef
,useImperativeHandle
,useLayoutEffect
,useDebugValue
. - And understand the endless potential of custom hooks.
useImperativeHandle
customizes the instance value that is exposed to parent components when using
React.jsref
While in useRef
we have seen how a parent element can get full access to a child component. useImperativeHandle
allows you to determine what the sub-component exposes, or set some of the element’s default behavior.
Syntax:
useImperativeHandle(ref, createHandle, [deps])
- ref: ref to the sub-component.
- createHandle: object returned from the sub-component to the parent.
- deps: something written only of the official syntax, but no one mentions :-\
Justification
Lets go back to the useRef
example:
const MyInput = React.forwardRef((props, ref)=>{
return <input {...props} ref={ref} />
})
const App = ()=>{
const myInputRef = React.useRef();
const myOnBlur = ()=> {
myInputRef.current.value = "Hi";
}
return <MyInput onBlur={myOnBlur} ref={myInputRef} />
}
Once we exposed the input ref, the parent component can do whatever he wants, and expand his control over out input:
const MyInput = React.forwardRef((props, ref)=>{
return <input {...props} ref={ref} />
})
const App = ()=>{
const myInputRef = React.useRef();
const myOnBlur = ()=> {
myInputRef.current.value = "Hi";
}
const myOnChange = ()=> {
myInputRef.current.value = "Ha ha!!!";
}
return <MyInput onBlur={myOnBlur} onChange={myOnChange} ref={myInputRef} />
}
Simple use case
For cases we want in implicit control of our sub-component behavior, we can use useImperativeHandle
to return a sealed list of actions or values:
const MyInput = React.forwardRef((props, ref) =>{
const internalRef = React.useRef();
React.useImperativeHandle(ref,()=>({
allowedAction: () => {
internalRef.current.value = "Hi";
}
}));
return <input ref={internalRef} {...props} />
});
const App = ()=>{
const inputRef = React.useRef();
const onMyBlur = ()=> {
inputRef.current.allowedAction();
}
return <MyInput onBlur={onMyBlur} ref={inputRef} />
}
In this case, the parent controller doesn’t have access to the sub-controller, but can only execute the exposed allowedAction method.
One more example: enriching an input to have some build-in options, like in this case, change the background color on blur.
const MyInput = React.forwardRef((props, ref) =>{
const internalRef = React.useRef();
React.useImperativeHandle(ref,()=>({
changeBackgroundColor: (color) => {
internalRef.current.style.backgroundColor = color;
}
}));
return <input ref={internalRef} {...props} />
});
const App = ()=>{
const inputYellowRef = React.useRef();
const inputRedRef = React.useRef();
const myOnBlur = (ref,color)=> {
ref.current.changeBackgroundColor(color);
}
return <>
<MyInput onBlur={()=>myOnBlur(inputYellowRef,'yellow')} ref={inputYellowRef} />
<MyInput onBlur={()=>myOnBlur(inputRedRef,'red')} ref={inputRedRef} />
</>
}
Enrich component output
In the same way, we can expose only specific inner values. In this case, we have a counter that exposes two values: the value of the counter and a status line.
const Counter = React.forwardRef((props, ref) => {
const [counter,setCounter] = React.useState(0);
const [changes,setChanges] = React.useState(0);
React.useImperativeHandle(ref, () => ({
value: counter,
status: `Got ${counter} after ${changes} changes`
}));
const counterAction = (val)=>{
setCounter(counter + val);
setChanges(changes + 1);
setTimeout(()=>{
props.onCounterChange();
},0);
}
return (
<>
<span onClick={()=>counterAction(1)}>⬆️</span>
{counter}
<span onClick={()=>counterAction(-1)}>⬇️</span>
</>
);
});
And use it by:
const App = ()=>{
const [counter,setCounter] = React.useState(0);
const [status,setStatus] = React.useState(0);
const counterRef = React.useRef();
const counterChanged = ()=>{
setCounter(counterRef.current.value);
}
const getStatus = ()=>{
setStatus(counterRef.current.status);
}
return <>
<p>
<Counter ref={counterRef} onCounterChange={counterChanged} />
{counter}
<button onClick={getStatus}>Get status</button>
{status}
</p>
</>
}
Note: unfortunately, as mentioned, I haven’t yet seen someone mention the deps
value of useImperativeHandle. But once found some reference to it, Ill update the post.