17. Refs in React

Overview

This chapter will introduce you to how to use references in React. You will be able to apply the knowledge gained from the chapter to implement different ways of applying references. By the end of this chapter, you will be able to use Refs in your existing React application in an effective way.

Introduction

In the previous chapters, we learned about the various methods we can use to fetch data from external APIs and how to include that data inside your React application. This process required you to send XMLHttpRequest(XHR) requests over the network, take the result, and store it in your component.

A web page is often composed of more than just a React app; for example, an application can be embedded in a website where there are various elements in a web page (including our React app) all working together, like different form elements or HTML components. With all these elements that live outside your React components, you might think: could we access these other elements that are not controlled by the React application itself?

This question leads to the following section. We actually can control these elements "living outside" of a React application through References(Refs).

Why React Refs?

In React, we use Refs to access, manipulate, and interact with the DOM directly. Direct references to HTML elements allow us to perform tasks that are commonly encountered when creating client-side applications. Those tasks include, but are not limited to:

  • Handling native events on DOM elements, such as focus and hover
  • Measuring DOM element dimensions inside the browser directly
  • Locking scrolling on view containers outside the React app

In this chapter, you will learn about the usage of Refs, different methods for how and where to apply them, and finally, how to abstract and hide their implementation details.

In order to understand the purpose of using React Refs, we have to first understand what DOM elements are and the meaning of references in relation to them.

References

For JavaScript to interact with and manipulate a DOM element, it needs a reference to that element. This reference is simply an object representation of a DOM element. Through the properties of the reference, we can read and write to the element's attributes, which, in turn, define its appearance and behavior in the browser.

You might wonder why we would want to manipulate a DOM node directly if we are already creating these using React in the first place. Refs can and should be used, when either the element that we wish to handle was created outside our app's scope or when the React element's properties do not suffice to achieve a specific goal.

Let's take a look at one of the scenarios that cannot be achieved with the properties provided by the React element as the first exercise of this chapter. In the following exercise, we will look into creating custom upload buttons.

Exercise 17.01: Creating Custom Upload Buttons with Refs

In this exercise, we will use a Ref to create a styled input button, which could not be styled using plain CSS. We will first create the file input and then add a custom button component that will trigger the file upload functionality of the file input when it is clicked. To do that, let's perform the following steps:

  1. Create a new React app using the create-react-app CLI:

    $ npx create-react-app upload-button

  2. Move into the new React applications folder and start the create-react-app development server:

    $ cd upload-button/ && npm start

  3. Inside the App.js file, change the App component to only return a plain input component:

    import React from "react";

    class App extends React.Component {

     render() {

     return <input />;

     }

    }

    export default App;

  4. To make the input trigger the browser's file upload functionality, add the file type to the input component:

    import React from "react";

    class App extends React.Component {

      render() {

        return <input type="file" />;

      }

    }

    export default App;

    When we run the application at http://localhost:3000, we should see the following output:

    Figure 17.1: App output

    Figure 17.1: App output

    If you click this input button, you should be prompted by the browser to select a file to upload.

  5. Below the input component, add a button component and add the styles for a gray background and white font color:

    class App extends React.Component {

      render() {

      return (

        <div>

        <input type="file" />

        <button style={{ backgroundColor: "gray", color: "white" }}>

          upload document

        </button>

       </div>

      );

      }

    }

    Note that you have to wrap the input and button components inside a div element, because each React component is required to have a single root sub-component. This div element is only used to display the buttons in the view layer.

  6. Pass it the hidden attribute in order to hide the native input file element:

    class App extends React.Component {

      render() {

        return (

        <div>

        <input type="file" hidden={true} />

        <button style={{ backgroundColor: "gray", color: "white" }}>

          upload document

        </button>

       </div>

      );

      }

    }

    To make the button component trigger the upload prompt, you have to create a Ref to the input component and then trigger the native click function on this reference.

  7. Start by creating a class field called inputRef to store the Ref:

    class App extends React.Component {

      inputRef;

      render() {

      return (

        <div>

        <input type="file" hidden={true} />

        <button style={{ backgroundColor: "gray", color: "white" }}>

          upload document

        </button>

       </div>

      );

     }

    }

  8. Create an inline function that takes a parameter called refParam and assign this parameter to the inputRef class field of the App component. Pass this function to the input component using the Ref prop:

    class App extends React.Component {

      inputRef;

      render() {

      return (

        <div>

        <input

          ref={refParam => this.inputRef = refParam}

          type="file" hidden={true}

        />

        <button style={{ backgroundColor: "gray", color: "white" }}>

          upload document

        </button>

        </div>

      );

      }

    }

  9. Lastly, add an onClick handler to the button component. This onClick handler should call the click function on the inputRef class field reference to the input component:

    class App extends React.Component {

      inputRef;

      render() {

      return (

        <div>

        <input

          ref={refParam => this.inputRef = refParam}

          type="file" hidden={true}

         />

        <button

          onClick={() => this.inputRef.click()}

          style={{ backgroundColor: "gray", color: "white" }}>

          upload document

        </button>

       </div>

      );

      }

    }

    The output is as follows:

    Figure 17.2: The upload button shown on the app

Figure 17.2: The upload button shown on the app

Now, click the button component and you will be prompted with the file upload window, because the custom button now triggers the file input's native functionality.

As you can observe, you have utilized a React Ref to bind the functionality of a native input that is hardly customizable to a button component that you can style as you wish. Now, after this first success, we should have a closer look at what you have just done. In particular, we will inspect the function that was passed as the ref object to the input component.

Now that we have practiced using Refs in a hands-on exercise, let's take a look at the various ways of creating Refs.

Ways of Creating React Refs

There are thee approaches to creating React Refs:

  • Using a callback function

    The first approach is used on class-based components where you can save the reference to a class field. In the previous exercise, we followed this approach when we created a Ref by passing a callback function as the reference to an input element:

    <input ref={refParam => this.inputRef = refParam} />

    This works because the ref object is automatically called by React when rendering and passes a reference to the created DOM element back into this function. We then persisted this reference into a class field called inputRef, so that it is accessible everywhere in the component's instance.

  • Creating a Ref using the React.createRef function

    As it turns out, the second way of creating a Ref also relies on class-based components. Unlike the callback type, here, the ref object is not only passed as an argument to the callback function but can also be initialized beforehand and can be populated as soon as React renders the component. To implement this manner, React provides a utility function called createRef. This method creates a reference that we can assign to a class field and pass it to the component we want to access though the ref. The following code snippet shows how a ref is created:

    class App extends React.Component {

    inputRef = React.createRef();

    render() {

      return (

        <input ref={this.inputRef} />

      );

      }

    }

    As we can see from the preceding code, the return value of the React.createRef function is an object with a single key called current. This current key, when initialized, is either empty or contains the values that were passed to the createRef function. The value of the current key will be replaced with the reference to the selected DOM element.

    This would require us to change the code from the previous exercise to not calling the native click function on the Ref directly, but rather on the Ref.current object, as shown in the following code:

    <button

      onClick={() => this.inputRef.current.click()}

      style={{ backgroundColor: "gray", color: "white" }}

      >

      upload document

    </button>

  • Initializing a Ref using the React.useRef hook

    The last approach to creating React Refs is by using a React hook and can therefore only be used with functional components.

    In our existing code example, we could restructure our class-based component to a function and leverage the useRef hook to achieve the same result of a button triggering another DOM element's functionality through a reference:

    const App = () => {

      const inputRef = React.useRef();

      return(

      <div>

        <input ref={inputRef} type="file" hidden={true} />

        <button

        onClick={() => inputRef.current.click()}

        style={{ backgroundColor: "gray", color: "white" }}>

          upload document

        </button>

        </div>

      )

    }

    The result of applying this useRef hook is very similar to the createRef utility function. That is, the return value is the same, namely, an object with only one key called current. And the value of current is either null or the value that was passed as a parameter to the useRef hook. Yet there is one key difference that makes the useRef hook more suitable for functional components; useRef, as opposed to createRef, preserves the reference across renders. This means that even if the function rendering a component is executed multiple times, the useRef hook always returns the same Ref prop that was created initially, regardless of how often the hook was triggered. All we did in the preceding code snippet was change the class syntax to a function syntax and use the React.useRef hook instead of the React. createRef function to initialize our ref.

Since we have explored the three ways to create references to DOM elements in our applications, we should go on and practice each of the approaches in an exercise.

We can use the class-based components and createRef using a very common use case for accessing HTML elements: to measure them, their height, and width.

Exercise 17.02: Measuring the Dimensions of a div Element in a Class-Based Component

In this exercise, we will create a merely visual div element and read its dimensions by creating a reference to this DOM node. Knowing the dimensions, such as the height, width, or offset of an element, comes in handy when this cannot be done using the properties, we access on React components. Therefore, it's necessary to access the DOM elements directly using references to them.

  1. Create a new React app using the create-react-app CLI:

    $ npx create-react-app custom-button

  2. Move into the new React applications folder and start the create-react-app development server:

    $ cd custom-button/ && npm start

  3. Inside the App.js file, change the App component to only return an empty div element:

    import React from "react";

    class App extends React.Component {

      render() {

        return <div />;

      }

    }

    export default App;

  4. To visualize the div, add a height, width, and border to the div style:

    import React from "react";

    class App extends React.Component {

      render() {

      return <div

        style={{ width: 80, height: 20, border: "4px solid black" }}

      />;

      }

    }

    export default App;

    You end up with a black-bordered box on your screen.

  5. To access the dimensions, you need to create a reference using the createRef function and assign it to a class field called divRef:

    import React from "react";

    class App extends React.Component {

      divRef = React.createRef();

      render() {

      return <div

        style={{ width: 80, height: 20, border: "4px solid black" }}

        />;

      }

    }

    export default App;

  6. To populate the empty divRef element, you have to pass the reference to the div element as the Ref prop:

    import React from "react";

    class App extends React.Component {

      divRef = React.createRef();

      render() {

      return <div

        ref={this.divRef}

        style={{ width: 80, height: 20, border: "4px solid black" }}

        />;

      }

    }

    export default App;

  7. Lastly, when clicking div, this should print the result of the getBoundingClientRect function on the current reference. This printed object holds the dimensions of the div element:

    import React from "react";

    class App extends React.Component {

      divRef = React.createRef();

      render() {

      return <div

        ref={this.divRef}

        onClick={() => console.log(

          this.divRef.current.getBoundingClientRect()

        )}

        style={{ width: 80, height: 20, border: "4px solid black" }}

      />;

      }

    }

    export default App;

    If you click on the div element, you will see a log to your browser console that looks similar to this one:

    DOMRect {x: 8, y: 8, width: 88, height: 28, top: 8, …}

    x: 8y: 8

    width: 88h

    eight: 28

    top: 8

    right: 96

    bottom: 36

    left: 8

These are the dimensions of the element in the browser. You might wonder why the height is 28 pixels instead of the 20 pixels that you defined in step 4. This happened because the actual height dimension is the sum of the element height and the height of the borders, which is 4 pixels each for the top and bottom borders. The same is true for all the other dimensions as well.

In this exercise, we used a reference to an element and measured its size and offsets, which otherwise cannot be done without accessing the DOM directly. For this exercise, we applied a class-based component. However, in this component, there is no state to be managed. Therefore, we could also use a functional component and reduce the slight code overhead caused by the class syntax. Let's change the structure in the next exercise.

Exercise 17.03: Measuring the Element Size in a Functional Component

This exercise will start where the previous exercise left off. Here, you will create a Ref in a functional component and use it to get the dimensions of a DOM element (information that is typically only accessible through the DOM directly). This will demonstrate how we can measure the dimensions of an element using a functional component. Therefore, you will not need to create yet another React application but can continue with the last output of Exercise 17.02, Measuring the Dimensions of a div Element in a Class-Based Component. The ref will give us access to the DOM element for the component, which is what we need to access the size of the rendered element in the browser:

  1. Delete the existing App component and create a new App component as a function that returns an empty div:

    import React from 'react'

    const App = () => {

      return <div />

    }

    export default App;

  2. Next, add the same styling to the div component as in the previous exercise:

    const App = () => {

      return <div

        style={{ height: 20, width: 80, border: "4px solid black" }}

      />

    }

  3. Create a reference using the useRef hook, assign the return value to a variable called divRef, and pass this variable to div as the Ref prop:

    const App = () => {

      const divRef = React.useRef();

      return(

        <div

          ref={divRef}

          style={{ height: 20, width: 80, border: "4px solid black" }}

        />)

    }

  4. Once again, pass an onClick handler, which displays the result of the getBoundingClientRect method of the current divRef reference:

    const App = () => {

      const divRef = React.useRef();

      return (

        <div

        ref={divRef}

        onClick={() => console.log( divRef.current.getBoundingClientRect())}

        style={{ height: 20, width: 80, border: "4px solid black" }}

        />)

    }

    When clicking the div element, the exact same dimensions as at the end of Exercise 17.02, Measuring the Dimensions of a div Element in a Class-Based Component will be  displayed:

    DOMRect {x: 8, y: 8, width: 88, height: 28, top: 8, …}

    x: 8

    y: 8

    width: 88

    height: 28

    top: 8

    right: 96

    bottom: 36

    left: 8

So far, we have seen how to create and pass Ref when the element that we want to interact with via a reference is created within the render method of the component that also uses this Ref. But what if we wanted to access a reference of a component that is wrapped by a reusable higher-order component (HOC) and this HOC simply adds functionality to its children components by caring about Ref objects? In such a case, we would not want to change the HOC in such a way that it handles the implementation detail of manipulating the reference.

In order to preserve the encapsulation of components, we can use a proxying technique referred to as Forwarding Refs.

Forwarding Refs

In this section, we are going to discuss the methodologies used in order to implement Forwarding Refs. These are as follows:

Composition

One of the key concepts of React is the composition of components. That means we want to be able to encapsulate logic without creating dependencies between components.

Let's look at the following code snippet, in which the SayHello component adds a button that says hello to literally any child component that you pass to it. This way, the child component only needs to know about what it is supposed to know, in the same way the SayHello component only cares about the logic and the UI responsible for saying hello:

import React from "react";

const SayHello = props => {

  return (

  <div>

    { props.children }

    <button onClick={() => console.log("Hello!")}>

      Say Hello!

    </button>

  </div>

  )

}

const App = () => {

  return (

    <SayHello>

      <p>I wish I could say hello...</p>

    </SayHello>

  )

}

export default App;

Following this example, we could replace the paragraph wrapped by the SayHello component with anything else that we want.

However, if we want to access the button component that is created in the SayHello component from outside, we can't do that because the components are composed.

Luckily, React provides a utility function to get hold of the reference inside another component as well. This utility is named React.forwardRef and lets us pass a reference through a proxy to a child component that we wish to interact with.

To proxy a Ref to the SayHello button, we need to first wrap the functional component inside the React.forwardRef utility, as shown in the following code:

const SayHello = React.forwardRef(props => {

  return (

  <div>

    { props.children }

    <button onClick={() => console.log("Hello!")}>

    Say Hello!

    </button>

  </div>

  )

})

Unlike all other functional components, forwardRef not only takes a single props parameter, but also a second parameter, which obviously holds the value of the reference we want to pass. We will call this secondary parameter forwardedRef, and pass it to the button component inside the render method of the SayHello component:

const SayHello = React.forwardRef((props, forwardedRef) => {

  return (

  <div>

    { props.children }

    <button

      ref={forwardedRef}

      onClick={() => console.log("Hello!")}

    >

    Say Hello!

    </button>

  </div>

  )

})

This way, we would be able to access the Say Hello button from the App component. With this forwarded reference, we could mimic trigger the Say Hello button from another button in the App component, as shown in the following code:

const SayHello = React.forwardRef((props, forwardedRef) => {

  return (

  <div>

    { props.children }

    <button

    ref={forwardedRef}

    onClick={() => console.log("Hello!")}

    >

    Say Hello!

    </button>

  </div>

  )

})

const App = () => {

  const buttonRef = React.useRef();

  

  return (

  <SayHello ref={buttonRef}>

    <p>I wish I could say hello...</p>

    <button

    onClick={() => buttonRef.current.click()}

    >

    Also say hello!

    </button>

  </SayHello>

  )}

The output is as follows:

Figure 17.3: The Sayhello component

Figure 17.3: The Sayhello component

As you can see from the above figure, forwarding the Ref from the App component is no different to passing the Ref as a prop, as we have already done in the preceding sections. We can now trigger the onClick handler of the Say Hello! button by clicking the button. It's even possible to pass the reference down multiple levels through multiple React.forwardRef functions.

Let's grab the opportunity to practice the forwarding of Ref objects. To do so, we will refer back to the previous exercise, where we measured the dimensions of an element; only this time, we will proxy the reference through another component.

Exercise 17.04: Measuring Button Dimensions Using a Forwarded Ref

In this exercise, we will create a custom component that encapsulates the styles of a button component. We will measure its dimensions from outside the custom component using useRef and forwardRef. Let's perform the following steps:

  1. Create a new React app using the create-react-app CLI:

    $ npx create-react-app proxy-styles

  2. Move into the new React applications folder and start the create-react-app development server:

    $ cd proxy-styles/ && npm start

  3. Inside the App.js file, delete the App component and create a new empty App component and also another empty component called StyledButton:

    import React from "react";

    const StyledButton = props => {

      return null;

    };

    const App = () => {

      return null;

    };

    export default App;

  4. Adapt the StyledButton component to return a native button component and pass the StyledButton children received as props inside:

    import React from "react";

    const StyledButton = props => {

      return <button>{props.children}</button>;

    };

    const App = () => {

      return null;

    };

    export default App;

  5. Inside the App component, use StyledButton to wrap any text you want, something like This is the button text:

    import React from "react";

    const StyledButton = props => {

      return <button>{props.children}</button>;

    };

    const App = () => {

      return <StyledButton>This is the button text</StyledButton>;

    };

    export default App;

  6. To really have a styled button, you have to pass styles to the button component inside StyledButton:

    import React from "react";

    const StyledButton = props => {

      return (

      <button

      style={{ backgroundColor: "grey", color: "white" }}

      >

        {props.children}

      </button>

      );

    };

    const App = () => {

      return <StyledButton>This is the button text</StyledButton>;

    };

    export default App;

  7. When opening your App component in the browser, you should see your styled button. Next, in order to display the button's dimensions, you need to first turn the StyledButton component from a functional component to a forwardRef call that takes the functional component as a parameter:

    const StyledButton = React.forwardRef((props, forwardedRef) => {

    return (

    <button style={{ backgroundColor: "grey", color: "white" }}>

    {props.children}

    </button>

    );

    });

  8. Also, remember to add the second parameter to the functional component. Call the Ref parameter forwardedRef and pass it to the button component

    const StyledButton = React.forwardRef((props, forwardedRef) => {

      return (

      <button

        ref={forwardedRef}

        style={{ backgroundColor: "grey", color: "white" }}

      >

        {props.children}

      </button>

      );

    });

  9. Back in the App component, create a Ref using the useRef hook, assign the return value to a variable named buttonRef, and pass this variable to the StyledButton component as a Ref prop:

    const App = () => {

      const buttonRef = React.useRef();

      

      return (

      <StyledButton ref={buttonRef}>

      This is the button text

      </StyledButton>

      );

    };

  10. Finally, use the useEffect hook inside the App component to log the getBoundingClientRect method of the current reference, buttonRef:

    const App = () => {

      const buttonRef = React.useRef();

      React.useEffect(() => {

      console.log(buttonRef.current.getBoundingClientRect());

      });

      return <StyledButton ref={buttonRef}>This is the button   text</StyledButton>;

    };

    Simply running this code in your browser will display the following DOMRect object in the console:

    DOMRect

    x: 8

    y: 9

    width: 124.359375

    height: 18

    top: 9

    right: 132.359375

    bottom: 27

    left: 8

This might be familiar to you, since this a similar output to what you saw in the previous exercises earlier. Specifically, these are the dimensions of the button element inside the StyledButton component. You got hold of these measurements by proxying a reference from the App component to the StyledButton component using the React.forwardRef function.

We will now put the knowledge we have acquired from this chapter to the test and will try our hands at solving the following activity.

Activity 17.01: Creating a Form with an Autofocus Input Element

Your task will be to create a user-date form that automatically focuses the first input field. You will begin by accessing plain input fields via React Refs. From there, you will move on to create a custom input component that can forward the reference to an input field. In the end, you will encapsulate the logic to auto focus and handle the ref by creating a controllable form component.

The following steps will help you to complete the activity:

  1. Create a new React app using the create-react-app CLI.
  2. Inside the App.js file, change the App component to only return a plain form component.
  3. Inside the form component, add three input fields: one for the first name, one for the last name, and a third one for the email.
  4. Create a reference for an input field as a class field and pass the reference to the first input field. On componentDidMount, trigger the focus function of the current Ref.
  5. Create a new function component called FocusableInput, which can forward a reference. Make this component simply return null.
  6. Now, instead of null, the FocusableInput component should now return an input component that is of the text type. This input takes the placeholder prop passed to FocusableInput. The input also gets passed the forwardedRef parameter as a Ref prop.
  7. The first input field within the App component should now be replaced with an instance of the FocusableInput component. This FocusableInput should get passed both the reference and the placeholder that were passed to the original input field.

    Hint: You could replace all native input fields in the App component with the FocusableInput component. Just make sure not to pass the same Ref to all FocusableInput components. By the way, the Ref prop on the FocusableInput component is optional; you don't have to pass it.

  8. Create a new functional component called FocusableForm. This component takes over all of the logic from the App component's render function.
  9. The App component now only returns FocusableForm. Also, the componentDidMount function and the inputRef class field are no longer needed in the App component.
  10. In order to access the input field in the FocusableForm component, you have to create a new Ref and pass it to the first input field.
  11. Now, use the useEffect hook to trigger a focus on the current reference, inputRef.
  12. Make the FocusableForm component controllable by passing an autoFocus prop from the App component to FocusableForm and using the prop in the useEffect hook to decide whether to focus the reference.

    The final output is as follows:

Figure 17.4: Final output of the app

Figure 17.4: Final output of the app

Following this activity, you have a controllable component that hides all of the imperative code to handle the focus of an input component depending on whether or not a specific prop is passed to this component. This activity is a perfect use case on how to create clean logic to access DOM elements directly using Refs and even proxy them to abstract the complexity even further.

Note

The solution of this activity can be found on page 755..

Summary

In this chapter, we have covered a really interesting part of React. The reality is that Refs are not something you are regularly going to encounter unless you are responsible for creating component libraries or working with more design-focused elements.

It is an interesting part of React because, as you might have noticed, Refs do not really fit into React's declarative style. Every time we use Refs, we use them to tell the browser how to do things instead of what we want to see. While not necessarily a problem, it can get complicated and we need to be clear about our intent when using Refs.

The best we can do is to hide most of what we are doing with Refs and ensure that anyone using our code only interacts with our components via props. If we need to access details more closely related to how the browser renders the component (such as focus, scrolling, or dimensions), that will require Refs because React can't handle it without them, but ultimately, the consumer of our code should not be aware of how we achieved these things. In the next chapter, we will look at Refs in more detail.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset