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

useImperativeHandle

customizes the instance value that is exposed to parent components when using ref

React.js

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.