With the inclusions of hooks in react, we have at our disposition a handful of hooks to use which give us a lot of flexibility to create our apps in a more simpler and easy to maintain way.
In this post we are going to cover one hook that can give us a lot of versatility at the moment to work with manipulating the behaviour of our views, the useRef hook.
What does this hook do?
You may have been familiarized with React.createRef() function which allows us to create a DOM reference in order to access and manipulate a DOM element. Well useRef hook give us that same functionality but not only for that, it can be used to hold any mutable value which persist the same trough different renders.
So in a more simple explanation, useRef can be use as a container for a mutable value that will be the same in each render.
Now that we know the what this hook can do, let's see some examples:
useRef for DOM reference:
The general use of useRef as createRef will be often to access a DOM element to keep a reference to it.
For example we are going to create a ref to access to an input element and see what we can do.
We are going to use this template:
import React, { useRef } from "react";
const RefDomView = () => {
const inputRef = useRef(null);
const handleOnClick=()=>console.log(inputRef.current)
return (
<>
<input
type="text"
placeholder="WRITE EXAMPLE TEXT MY FRIEND"
ref={inputRef} />
<button onClick={handleOnClick}>See ref</button>
</>
)
}
We defined our ref with the constant inputRef using useRef with null since its value will be set when we defined our input text ref property with the inputRef. Now out inputRef has a reference to the input element.
When the button is clicked the handleOnClick function will be called and it will print what the inputRef holds:
We see that we have saved the input element and all the properties it holds, and since we know our useRef value is mutable we can also edit the properties of the input element, for example its text colour:
import React, { useRef } from "react";
const RefDomView = () => {
const inputRef = useRef(null);
const handleOnClick=()=>inputRef.current.style.color = "blue"
return (
<>
<input
type="text"
placeholder="WRITE EXAMPLE TEXT MY FRIEND"
ref={inputRef} />
<button onClick={handleOnClick}>Change Text Color</button>
</>
)
}
Now when the button is clicked, the input text colour will change to blue. As we can have access to the element properties we can call events like the focus function:
import React, { useRef } from "react";
const RefDomView = () => {
const inputRef = useRef(null);
const handleOnClick=()=>inputRef.current.focus()
return (
<>
<input
type="text"
placeholder="WRITE EXAMPLE TEXT MY FRIEND"
ref={inputRef} />
<button onClick={handleOnClick}>Change Text Color</button>
</>
)
}
For an extra example, resetting the input without losing the last saved value:
import React, { useState,useRef } from "react";
const RefDomView = () => {
const inputRef = useRef(null);
const [inputValue,setInputValue]=useState("")
const handleOnClick=()=>inputRef.current.value=""
return (
<>
<input
type="text"
value={inputValue}
onChange={(e)=>setInputValue(e.target.value)}
placeholder="WRITE EXAMPLE TEXT MY FRIEND"
ref={inputRef} />
<button onClick={handleOnClick}>Change Text Color</button>
</>
)
}
As we saw useRef can help us when we want to control the behaviour of an DOM element, but we can use it for other cases.
Referencing third party component libraries
When it comes to components that come from a third party library, we can use the hook only if the library does a technique called Ref Forwarding, if not the ref won't actually have any use.
To know if the library uses it, you can check its documentation or check is the prop ref shows as recommendation when you are working on an IDE.
useRef To Hold Values:
We can store any value we want with useRef and use it as any other variable but the this stored value will survive all the react lifecycle, meaning it will keep the same value on every render if we didn't change it manually. However we need to keep in mind that useRef value won't notify when its content changes.
So when it come to useRef to hold values it works better when we use it as a control property. For example a common use is to build a componentDidUpdate function when using hooks.
import React, { useState, useEffect, useRef } from "react";
const RefValueView = () => {
const selectedRef = useRef({ name: "", type: "" });
const [selectedValue, setSelectedValue] = useState({
name: " ",
type: " "
});
const [message, setMessage] = useState(0);
const optionList = [
{ name: "bulbasaour", type: "grass" },
{ name: "charmander", type: "fire" },
{ name: "squirtle", type: "water" },
{ name: "bulbasaour", type: "grass" }
];
useEffect(() => {
if (selectedRef.current.type !== selectedValue.type) {
changeMessage();
selectedRef.current = selectedValue;
}
}, [selectedValue]);
const changeMessage = () =>
setMessage(`Random number ${Math.random()}`);
return (
<div className="container-card ref-value-view">
<label>{message}</label>
<select
onChange={({ target: { value } }) => {
setSelectedValue(optionList[value]);
}}
>
{optionList.map((option, index) => (
<option key={index} value={index}>
{option.name}
</option>
))}
</select>
</div>
);
};
In example above we set a optionList to be rendered as the options of the select element with the index as the value and the property name as the display text. When the selection changes we get the item on the selected index and saved it on the selectedOption state.
The trick comes here, the useState with the selectedOption as it dependency will execute everything inside it when the component mounts or when the selectedOption value changes. The problem lies on the useEffect when selectedOption changes, useEffect only does a shallow comparison two see if the value really has changed. Since this comparison only works when checking primitives (numbers,strings,boolean,etc) when using objects useEffect will always run when it changes since it compares its references and not its values.
The workaround to verify if the value has changed is to store the previous state on a useRef and check if the old properties are equal to the new ones and execute the internal code when they are different. In the case the properties are different the function setMessage() will be called and display a new number, if not the number won't change.
Another way to use it
In same logic we use the useRef to detect when a value has changed we can use it to control when the the useEffect code will run. Let's say we only want this code to run when a dependency changes but not when component mounts.
import React, { useState, useEffect, useRef } from "react";
const AvoidMountView=()=>{
const isMounted=useRef(false)
const [number,setNumber]=useState(0)
useEffect(()=>{
if(!isMounted){
isMounted.current = true
}else{
console.log(100/number)
}
},[number])
return (
<input
value={number}
min="1"
max="100"
type="number"
onChange={(e)=>setNumber(e.target.value}/>
)
}
If we have some code that we don't want to call when the component mounts, like in the example above where if the code runs on mount it will log infinity because of the division by 0 (that was done on purpose as an example). In this case we can use the useRef as a flag to verify when the component is already mounted and then run the code. In the first call of useEffect that occurs on the mount part, isMounted is false so by just checking it we changed its value to true, now the useEffect will only run when number changes.
With this usage, the ref can become a useful debug help by keeping track of the changes on the mount/unmount process and also as it keep the same value on every render, to check for changes when unnecessary renders happens.
Forwarding Ref
Let's say we are working with two components, and we want to divide our logic to control the view behaviour and let the children components juts work as simple as possible. With ref there is an application called RefForwarding which allow us to transfer the ref from the component to one of its children.
Lets see a little example:
import React from "react";
const Input =() => <input type="text"/>;
export default Input;
import React from "react";
import Input from "./input";
export default function Button() {
const handleClick = (e) => {
console.log("clicked lol")
};
return (
<>
<Input />
<button onClick={handleClick}>Toggle Type</button>
</>
);
}
We have two components, Button which will be our parent component which will handle the logic and the Input component which will be for presentation. To use our ref in order to control the child component input we implement a React function called fordwardRef().
import React from "react";
const Input = React.forwardRef((props, ref) => <input type="text" ref={ref} />);
export default Input;
The forwardRef function has to parameter, the props of the component and the ref that comes from its parent. With this the parent element has access to its child referenced element to handle or observe the element's properties.
import React, { useRef } from "react";
import Input from "./input";
export default function Button() {
const inputRef = useRef(null);
const handleClick = (e) => {
//show we are referencing the input element
console.log(inputRef.current);
inputRef.current.focus();
const type = inputRef.current.type === "text" ? "number" : "text";
inputRef.current.type = type;
};
return (
<>
<Input ref={inputRef} />
<button onClick={handleClick}>Toggle Type</button>
</>
);
}
We create the ref as usual and pass it to the child component ref property. Now that the parent have access to the element, it can control it using its own logic. In the Button example on the handleClick function, we excite two actions: focus the input and toggle its type from 'text' to 'number'.
As a little extra, we can give the forwarded ref a name for an easier identification when using React DevTools.
import React from "react";
const Input = React.forwardRef(function InputForwardRef(props,ref){
return <input type="text" ref={ref} />
});
export default Input;
The DevTools will show the ref with the name of the function
You cant also give a custom name by adding the property displayName to the function
import React from "react";
function InputForwardRef(props, ref) {
return <input type="text" ref={ref} />;
}
InputForwardRef.displayName = "CUSTOM FORWARDREF NAME";
const Input = React.forwardRef(InputForwardRef);
export default Input;
Forwarding your refs if you are going to build a library
Ref Forwarding can be really useful when your are building a component library since you can provide extra control over the components when using them since you not only forward the element and its attributes but also the component props.
Some advices
We saw how we can implement the ref in our apps to make some control and interactivity easier, but as useful as ref are there some things to keep in mind:
- Don't abuse the use of ref to handle values, remember when a ref changes it won't notify it and a most of the time the interactivity between parent and child can be done by lifting up the state.
- Use the ref as control points like in when the useEffect should run example.
- Apply the ref to elements that you need to keep control of or elements that would be hard to reach searching through the DOM.
- Element's styles and attributes can be manipulated with some simple js (by using component state o accessing the DOM) or css code, use the ref when it makes it easier to manipulate between your app logic.
- Ref are great when it comes to debug yout app when it comes to check when or why unnecessary renders happens, check what changes when a re render occurs, keep track of the mount/unmount process.