Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
April 4, 2023 07:15 am GMT

Refs in React: from access to DOM to imperative API

Image description

Originally published at https://www.developerway.com. The website has more articles like this

One of the many beautiful things about React is that it abstracts away the complexity of dealing with real DOM. Now, instead of manually querying elements, scratching our heads over how to add classes to those elements, or struggling with browser inconsistencies, we can just write components and focus on user experience. There are, however, still cases (very few though!) when we need to get access to the actual DOM.

And when it comes to actual DOM, the most important thing to understand and learn how to use properly is Ref and everything surrounding Ref. So today, lets take a look at why we would want to get access to DOM in the first place, how Ref can help with that, what are useRef, forwardRef and useImperativeHandle, and how to use them properly. Also, lets investigate how to avoid using forwardRef and useImperativeHandle while still having what they give us. If you ever tried to figure out how those work, youll understand why wed want that

And as a bonus, well learn how to implement imperative APIs in React!

This article is also available as a YouTube video: it has fewer details but nice animations instead. Sometimes it's easier to understand a concept from a 3-second animation than from two paragraphs of text.

DOM access in React with useRef

Lets say I want to implement a sign-up form for a conference Im organizing. I want people to give me their name, email and Twitter handle before I can send them the details. Name and email fields I want to make mandatory. But I dont want to show some annoying red borders around those inputs when people try to submit them empty, I want the form to be cool. So instead, I want to focus the empty field and shake it a little to attract attention, just for the fun of it.

Now, React gives us a lot, but it doesnt give us everything. Things like focus an element manually is not part of the package. For that, we need to dust off our rusty native javascript API skills. And for that, we need access to the actual DOM element.

In the no-React world, wed do something like this:

const element = document.getElementById("bla");

After then we can focus it:

element.focus();

Or scroll to it:

element.scrollIntoView();

Or anything else our heart desires. Some typical use cases for using native DOM API in React world would include:

  • manually focusing an element after its rendered, like an input field in a form
  • detecting a click outside of a component when showing popup-like elements
  • manually scrolling to an element after it appears on the screen
  • calculating sizes and boundaries of components on the screen to position something like a tooltip correctly

And although, technically, nothing is stopping us from doing getElementById even today, React gives us a slightly more powerful way to access that element that doesnt require us to spread ids everywhere or be aware of the underlying DOM structure: refs.

Ref is just a mutable object, the reference to which React preserves between re-renders. It doesnt trigger re-render, so its not a replacement to state in any way, dont try to use it for it. More details on the difference between those two are in the docs.

Its created with useRef hook:

const Component = () => {  // create a ref with default value null  const ref = useRef(null);  return ...}

And the value stored in Ref will be available in current (and only) property of it. And we can actually store anything in it! We can, for example, store an object with some values coming from state:

const Component = () => {  const ref = useRef(null);  useEffect(() => {    // re-write ref's default value with new object    ref.current = {      someFunc: () => {...},      someValue: stateValue,    }  }, [stateValue])  return ...}

Or, more importantly for our use case, we can assign this Ref to any DOM element and some of the React components:

const Component = () => {  const ref = useRef(null);  // assing ref to an input element  return <input ref={ref} />}

Now, if I log ref.current in useEffect (its only available after a component is rendered), Ill see exactly the same element that I would get if I try to do getElementById on that input:

const Component = () => {  const ref = useRef(null);  useEffect(() => {    // this will be a reference to input DOM element!    // exactly the same as if I did getElementById for it    console.log(ref.current);  });  return <input ref={ref} />}

And now, if I was implementing my sign-up form as one giant component, I could do something like this:

const Form = () => {  const [name, setName] = useState('');  const inputRef = useRef(null);  const onSubmitClick = () => {    if (!name) {      // focus the input field if someone tries to submit empty name      ref.current.focus();    } else {      // submit the data here!    }  }  return <>    ...    <input onChange={(e) => setName(e.target.value)} ref={ref} />    <button onClick={onSubmitClick}>Submit the form!</button>  </>}

Store the values from inputs in state, create refs for all inputs, and when submit button is clicked, I would check whether the values are not empty, and if they are - focus the needed input.

Check out this form implementation in this codesandbox:

Passing ref from parent to child as a prop

Only in real life, I wouldnt do one giant component with everything of course. More likely than not, I would want to extract that input into its own component: so that it can be reused across multiple forms, and can encapsulate and control its own styles, maybe even have some additional features like having a label on the top or an icon on the right.

const InputField = ({ onChange, label }) => {  return <>    {label}<br />    <input type="text" onChange={(e) => onChange(e.target.value)} />  </>}

But error handling and submitting functionality still going to be in the Form, not input!

const Form = () => {  const [name, setName] = useState('');  const onSubmitClick = () => {    if (!name) {      // deal with empty name    } else {      // submit the data here!    }  }  return <>    ...    <InputField label="name" onChange={setName} />    <button onClick={onSubmitClick}>Submit the form!</button>  </>}

How can I tell input to focus itself from the Form component? The normal way to control data and behaviour in React is to pass props to components and listen to callbacks. I could try to pass the prop focusItself to InputField that I would switch from false to true, but that would work only once.

// don't do this! just to demonstate how it could work in theoryconst InputField = ({ onChange, focusItself }) => {  const inputRef = useRef(null);  useEffect(() => {    if (focusItself) {      // focus input if the focusItself prop changes      // will work only once, when false changes to true      ref.current.focus();    }  }, [focusItself])  // the rest is the same here}

I could try to add some onBlur callback and reset that focusItself prop to false when input loses focus, or play around with random values instead of boolean, or come up with some other creative solution.

Likely, there is another way. Instead of fiddling around with props, we can just create Ref in one component (Form), pass it down to another component (InputField), and attach it to the underlying DOM element there. Ref is just a mutable object, after all.

Form would then create the Ref as normal:

const Form = () => {  // create the Ref in Form component  const inputRef = useRef(null);  ...}

And InputField component will have a prop that accepts the ref, and will have input field that accepts the ref, as usual. Only Ref, instead of being created in InputField, will be coming from props there:

const InputField = ({ inputRef }) => {  // the rest of the code is the same  // pass ref from prop to the internal input component  return <input ref={inputRef} ... />}

Ref is a mutable object, was designed that way. When we pass it to an element, React underneath just mutates it. And the object that is going to be mutated is declared in the Form component. So as soon as InputField is rendered, Ref object will mutate, and our Form will have access to the input DOM element in inputRef.current:

const Form = () => {  // create the Ref in Form component  const inputRef = useRef(null);  useEffect(() => {    // the "input" element, that is rendered inside InputField, will be here    console.log(inputRef.current);  }, []);  return (    <>      {/* Pass ref as prop to the input field component */}      <InputField inputRef={inputRef} />    </>  )}

or in our submit callback we can call the inputRef.current.focus(), exactly the same code as before.

Check out the example here:

Passing ref from parent to child with forwardRef

In case youre wondering why I named the prop inputRef, rather than just ref: its actually not that simple. ref is not an actual prop, it's kinda a reserved name. In the old days, when we were still writing class components, if we passed ref to a class component, this components instance would be the .current value of that Ref.

But functional components dont have instances. So instead, we just get a warning in console Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

const Form = () => {  const inputRef = useRef(null);  // if we just do this, we'll get a warning in console  return <InputField ref={inputRef} />}

In order for this to work, we need to signal to React that this ref is actually intentional, we want to do stuff with it. We can do it with the help of forwardRef function: it accepts our component and injects the ref from the ref attribute as a second argument of the components function. Right after the props.

// normally, we'd have only props there// but we wrapped the component's function with forwardRef// which injects the second argument - ref// if it's passed to this component by its consumerconst InputField = forwardRef((props, ref) => {  // the rest of the code is the same  return <input ref={ref} />})

We could even split the above code into two variables for better readability:

const InputFieldWithRef = (props, ref) => {  // the rest is the same}// this one will be used by the formexport const InputField = forwardRef(InputFieldWithRef);

And now the Form can just pass ref to the InputField component as it was a regular DOM element:

return <InputField ref={inputRef} />

Whether you should use forwardRef or just pass ref as a prop is just a matter of personal taste: the end result is the same.

See the live example here:

Imperative API with useImperativeHandle

Okay, focusing the input from the Form component is sorted, kinda. But we are in no way done with our cool form. Remember, we wanted to shake the input in addition to focusing when the error happens? There is no such thing as element.shake() in native javascript API, so access to the DOM element wont help here

We could very easily implement it as a CSS animation though:

const InputField = () => {  // store whether we should shake or not in state  const [shouldShake, setShouldShake] = useState(false);  // just add the classname when it's time to shake it - css will handle it  const className = shouldShake ? "shake-animation" : '';  // when animation is done - transition state back to false, so we can start again if needed  return <input className={className} onAnimationEnd={() => setShouldShake(false)} />}

But how to trigger it? Again, the same story as before with focus - I could come up with some creative solution using props, but it would look weird and significantly over-complicate the Form. Especially considering that we're handling focus through ref, so we'd have two solutions for exactly the same problem. If only I could do something like InputField.shake() and InputField.focus() here!

And speaking of focus - why my Form component still has to deal with native DOM API to trigger it? Isnt it the responsibility and the whole point of the InputField, to abstract away complexities like this? Why does the form even have access to the underlying DOM element - its basically leaking internal implementation details. The Form component shouldnt care which DOM element were using or whether we even use DOM elements or something else at all. Separation of concerns, you know.

Looks like its time to implement a proper imperative API for our InputField component. Now, React is declarative and expects us all to write our code accordingly. But sometimes we just need a way to trigger something imperatively. Likely, React gives us an escape hatch for it: useImperativeHandle hook.

This hook is slightly mind-boggling to understand, I had to read the docs twice, try it out a few times and go through its implementation in the actual React code to really get what its doing. But essentially, we just need two things: decide how our imperative API would look like and a Ref to attach it to. For our input, its simple: we just need .focus() and .shake() functions as an API, and we already know all about refs.

// this is how our API could look likeconst InputFieldAPI = {  focus: () => {    // do the focus here magic  },  shake: () => {    // trigger shake here  }}

This useImperativeHandle hook just attaches this object to Ref object's "current" property, that's all. This is how it does it:

const InputField = () => {  useImperativeHandle(someRef, () => ({    focus: () => {},    shake: () => {},  }), [])}

The first argument - is our Ref, which is either created in the component itself, passed from props or through forwardRef. The second argument is a function that returns an object - this is the object that will be available as inputRef.current. And the third argument is the array of dependencies, same as any other React hook.

For our component, lets pass the ref explicitly as apiRef prop. And the only thing that is left to do is to implement the actual API. For that wed need another ref - this time internal to InputField, so that we can attach it to the input DOM element and trigger focus as usual:

// pass the Ref that we'll use as our imperative API as a propconst InputField = ({ apiRef }) => {  // create another ref - internal to Input component  const inputRef = useRef(null);  // "merge" our API into the apiRef  // the returned object will be available for use as apiRef.current  useImperativeHandle(apiRef, () => ({    focus: () => {      // just trigger focus on internal ref that is attached to the DOM object      inputRef.current.focus()    },    shake: () => {},  }), [])  return <input ref={inputRef} />}

And for shake well just trigger the state update:

// pass the Ref that we'll use as our imperative API as a propconst InputField = ({ apiRef }) => {  // remember our state for shaking?  const [shouldShake, setShouldShake] = useState(false);  useImperativeHandle(apiRef, () => ({    focus: () => {},    shake: () => {      // trigger state update here      setShouldShake(true);    },  }), [])  return ...}

And boom! Our Form can just create a ref, pass it to InputField and will be able to do simple inputRef.current.focus() and inputRef.current.shake(), without worrying about their internal implementation!

const Form = () => {  const inputRef = useRef(null);  const [name, setName] = useState('');  const onSubmitClick = () => {    if (!name) {      // focus the input if the name is empty      inputRef.current.focus();      // and shake it off!      inputRef.current.shake();    } else {      // submit the data here!    }  }  return <>    ...    <InputField label="name" onChange={setName} apiRef={inputRef} />    <button onClick={onSubmitClick}>Submit the form!</button>  </>}

Play around with the full working form example here:

Imperative API without useImperativeHandle

If useImperativeHandle hook still makes your eye twitch - dont worry, mine twitches too! But we dont actually have to use it to implement that functionality that we just implemented. We already know how refs work, and the fact that they are mutable. So all that we need is just to assign our API object to the ref.current of the needed Ref, something like this:

const InputField = ({ apiRef }) => {  useEffect(() => {    apiRef.current = {      focus: () => {},      shake: () => {},    }  }, [apiRef])}

This is almost exactly what useImperativeHandle does under the hood anyway. And it will work exactly like before.

Actually, useLayoutEffect might be even better here, but this is the topic for another article. For now, let's go with traditional useEffect.

See the final example here:

Yey, a cool form with a nice shaking effect is ready, React refs are not a mystery anymore, and imperative API in React is actually a thing. How cool is that?

Just remember: Refs are an escape hatch, its not a replacement to state or normal React data flow with props and callbacks. Use them only when there is no normal alternative. The same with the imperative way to trigger something - more likely than not normal props/callbacks flow is what you want.

Watch the article in YouTube format to solidify the knowledge

Originally published at https://www.developerway.com. The website has more articles like this

Subscribe to the newsletter, connect on LinkedIn or follow on Twitter to get notified as soon as the next article comes out.


Original Link: https://dev.to/adevnadia/refs-in-react-from-access-to-dom-to-imperative-api-j2l

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To