React hooks and functional component ref
Asked Answered
I

3

6
const Comp1 = forwardRef((props, ref) => {
    useImperativeHandle(ref, () => ({
    print: () => {
      console.log('comp1')
    }
  }), []);
  return <div>comp1</div>
});

const Comp2 = () => <div>comp2</div>;

const App = () => {
  const ref1 = useRef(null);
  const ref2 = useRef(null);

  useEffect(() => {
    console.log(ref1); // prints ref1 with the expected current  
    console.log(ref2); // prints ref2 with current: null
  })
  return <div><Comp1 ref={ref1}/><Comp2 ref={ref2}/></div>
}
  1. What's the difference between Comp1 and Comp2 refs?
  2. Why do I have to use forwardRef along with useImperativeHandle in order to actually get the ref to Comp1?

https://codepen.io/benma/pen/mddEWjP?editors=1112

Impiety answered 16/10, 2019 at 11:1 Comment(2)
here is a good thread about the topic: twitter.com/acemarke/status/1182466648646832129Trev
@LefiTarik Thanks! I was actually looking for some info about this hook and couldn't find much,Impiety
S
6

React documentation says:

You may not use the ref attribute on function components because they don’t have instances. (more)

This means that you can't bind your refs to functional components. That's why your ref2.current is null. If you want to bind ref to component, you need to use class components. Your ref1 is not a ref to the Comp1 component. It actually contains an object that you passed in useImperativeHandle hook. i.e. it contains the next object:

{
    print: () => {
      console.log('comp1')
    }
}

You have to use forwardRef with functional components if you want to bind your ref with some HTML element or class component that your component renders. Or you could bind your ref with some object with using of useImperativeHandle hook.

UPDATE

The using of useImperativeHandle is the same as adding methods to your class component:

class Comp1 extends React.Component {
    print() {
        console.log('comp1');
    }

    render() {
        return (<div>comp1</div>)
    }
}

is the same as

const Comp1 = forwardRef((props, ref) => {
    useImperativeHandle(ref, () => ({
    print: () => {
      console.log('comp1')
    }
  }), []);
  return <div>comp1</div>
});

You asked in a comment:

So after moving to hooks (and still avoid using classes), the only way to use ref is to use useImperativeHandle (and actually use a "fake" ref)? Is this a good practice?

Answer: Using of useImperativeHandle is the same bad practice as calling child component methods by refs in class components. React doc says that you should avoid calling child component methods by refs, you should avoid using of useImperativeHandle. Also, you need to avoid using refs where you can do things without them.

Stradivarius answered 16/10, 2019 at 11:33 Comment(1)
So after moving to hooks (and still avoid using classes), the only way to use ref is to use useImperativeHandle (and actually use a "fake" ref)? Is this a good practice?Impiety
D
3

1. What's the difference between Comp1 and Comp2 refs?

Comp1 uses React.forwardRef and will be able to receive a given ref from parent.
Comp2 won't work and trigger following error:

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

<Child ref={ref1}/> is like a request from parent component: "Hey Child, please pass your component reference or similar into given mutable store box (ref), so I can invoke a method on you or manipulate a contained DOM node directly."

Problem - there is no instance for function components:
// React internally calls `new ClassComp()`, instance can be stored and passed in a ref
<ClassComp ref={compRef} />

// React just *calls* the function for re-renders, there is no plain function "instance" 
<FunctionComp ref={compRef} />
React.forwardRef solves above limitation. With
const FunctionComp = React.forwardRef((props, ref) => <div ref={ref}>Hello FnComp</div>

, FunctionComp can still pass something representative to the ref given from Parent (like div DOM node), despite having no instance. ClassComp instead passes its instance here.


2. Why do I have to use forwardRef along with useImperativeHandle in order to actually get the ref to Comp1?

You don't have to. useImperativeHandle is an extension to provide a more custom imperative call API. Following three alternatives are equivalent:

forwardRef only:

const App = () => {
  const compRef = React.useRef();
  return (
    <div>
      <Comp ref={compRef} />
      <button
        onClick={() => {
          compRef.current.focus();
        }}
      >
        Focus input
      </button>
    </div>
  );
}

const Comp = React.forwardRef((props, ref) => {
  const inputRef = React.useRef();
  return <input ref={ref} />;
});

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
useImperativeHandle only, with custom ref prop (take a look at this answer for more info):

const App = () => {
  const compRef = React.useRef();
  return (
    <div>
      <Comp customRef={compRef} />
      <button
        onClick={() => {
          compRef.current.focus();
        }}
      >
        Focus input
      </button>
    </div>
  );
}

const Comp = ({ customRef }) => {
  const inputRef = React.useRef();
  React.useImperativeHandle(customRef, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} />;
};

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
useImperativeHandle + forwardRef:

const App = () => {
  const compRef = React.useRef();
  return (
    <div>
      <Comp ref={compRef} />
      <button
        onClick={() => {
          compRef.current.focus();
        }}
      >
        Focus input
      </button>
    </div>
  );
}

const Comp = React.forwardRef((props, ref) => {
  const inputRef = React.useRef();
  React.useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} />;
});

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
Donniedonnish answered 5/6, 2020 at 10:9 Comment(0)
S
1

I will try to answer from question 2

  • useImperativeHandle described that the reference call syntax will be with some customized methods of the instance value that is exposed to parent.

  • forwardRef helps forward a ref through a Higher-order component to reference an inner DOM node.

when you will try to focus: ref1.current.focus(), the child input will be focused and not higher order component that wrapping the child input.

Look at simple example related to answer with setValue method of useImperativeHandle and forward input in ChildInput: https://codesandbox.io/s/react-hook-useimperativehandle-huktt

Seiden answered 16/10, 2019 at 11:27 Comment(1)
just to highlight: it has been working before hooks arrived. So forwareRef((prop, ref) => { .... ref.current = {setValue: (value) => ....} ) works as well. useImperativeHandle just encapsulate working with references and make some additional checks, say, in case callback-type reference.Girand

© 2022 - 2024 — McMap. All rights reserved.