14 - useRef hook
🔗 Understanding useRef
in React
The useRef
hook in React is a versatile tool that allows you to persist values between renders without triggering re-renders when those values change. It's commonly used for accessing and manipulating DOM elements directly, storing mutable values, and integrating with third-party libraries.
📌 What is useRef
?
useRef
is a React hook that returns a mutable ref object. This object has a .current
property that you can use to store any value, similar to how you might use instance fields in classes. Unlike state variables returned by useState
, updating a ref does not cause a component to re-render.
import { useRef } from 'react';
const Example = () => {
const myRef = useRef(initialValue);
// You can access or modify the value using myRef.current
};
🛠️ Common Use Cases
1. Accessing DOM Elements
useRef
is often used to access DOM elements directly. This is useful for tasks like focusing an input field, selecting text, or measuring the size of an element.
import React, { useRef } from 'react';
const FocusInput = () => {
const inputRef = useRef(null);
const focusInputField = () => {
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" placeholder="Focus me with the button" />
<button onClick={focusInputField}>Focus Input</button>
</div>
);
};
export default FocusInput;
2. Storing Mutable Values
useRef
can store values that persist across renders without triggering re-renders when updated. This is useful for keeping track of previous values, timers, or any mutable data.
import React, { useRef, useState, useEffect } from 'react';
const Timer = () => {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef(null);
useEffect(() => {
intervalRef.current = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
return () => clearInterval(intervalRef.current);
}, []);
return <div>Seconds Elapsed: {seconds}</div>;
};
export default Timer;
3. Integrating with Third-Party Libraries
When working with libraries that require direct DOM manipulation or need to access DOM nodes, useRef
provides a way to reference those nodes.
import React, { useRef, useEffect } from 'react';
import SomeThirdPartyLibrary from 'third-party-library';
const ThirdPartyComponent = () => {
const containerRef = useRef(null);
useEffect(() => {
const instance = new SomeThirdPartyLibrary(containerRef.current);
instance.initialize();
return () => {
instance.destroy();
};
}, []);
return <div ref={containerRef}></div>;
};
export default ThirdPartyComponent;
📝 Example: Login Component Using useRef
Let's explore a practical example of how useRef
can be utilized within a React component, specifically a simplified Login component.
📄 Login Component Code
import Header from "./Header";
import bgImage from "../assets/large-bg-image.jpg";
import { useRef, useState } from "react";
import { validateData } from "../utils/validate";
const Login = () => {
const [isSignIn, setIsSignIn] = useState(true);
const [errors, setErrors] = useState({});
const emailRef = useRef();
const passwordRef = useRef();
const fullnameRef = useRef();
const toggleForm = () => setIsSignIn(!isSignIn);
const handleSubmit = () => {
const formData = {
email: emailRef.current.value,
password: passwordRef.current.value,
fullname: isSignIn ? undefined : fullnameRef.current.value,
};
const validationErrors = validateData(formData, isSignIn);
setErrors(validationErrors);
};
return (
<div
className="bg-center bg-cover h-screen"
style={{ backgroundImage: `url(${bgImage})` }}
>
<Header />
<div>
<div>
<h1>
{isSignIn ? "Sign In" : "Sign Up"}
</h1>
<form
onSubmit={(e) => e.preventDefault()}
>
<div>
<input
ref={emailRef}
type="email"
placeholder="Email Address"
/>
{errors.email && <p>{errors.email}</p>}
</div>
{!isSignIn && (
<div>
<input
ref={fullnameRef}
type="text"
placeholder="Full Name"
/>
{errors.fullname && <p>{errors.fullname}</p>}
</div>
)}
<div>
<input
ref={passwordRef}
type="password"
placeholder="Password"
/>
{errors.password && <p className="text-red-500 text-sm">{errors.password}</p>}
</div>
<button
type="submit"
className="w-full bg-red-600 text-white p-2 rounded hover:bg-red-700"
onClick={handleSubmit}
>
{isSignIn ? "Sign In" : "Sign Up"}
</button>
</form>
<p className="text-gray-300 text-sm text-center">
{isSignIn ? "New here?" : "Already have an account?"}
<span
className="text-white ml-1 cursor-pointer"
onClick={toggleForm}
>
{isSignIn ? "Sign Up" : "Sign In"}
</span>
</p>
</div>
</div>
</div>
);
};
export default Login;
📊 How useRef
is Utilized in the Login Component
In the Login
component, useRef
is employed to manage form input fields efficiently. Here's a breakdown of how it's used:
-
Creating References:
emailRef
: References the email input field.passwordRef
: References the password input field.fullnameRef
: References the fullname input field (only visible during sign-up).
-
Attaching References to DOM Elements:
Each input field is linked to its corresponding ref using the
ref
attribute. -
Accessing Input Values:
When the user submits the form by clicking the button, the
handleSubmit
function accesses the current values of each input field using the.current.value
property of the respective refs. -
Advantages in This Context:
- Simplified Form Handling: By using refs, there's no need to bind state variables to each input field, reducing the complexity of state management.
- Performance Optimization: Since updating refs doesn't trigger re-renders, the component remains performant, especially beneficial for forms with many fields.
- Direct Access: Provides straightforward access to input values for validation or other operations without intermediary steps.
⚖️ useRef
vs. useState
While both useRef
and useState
can hold values, they serve different purposes:
useState
:- Ideal for values that influence what gets rendered.
- Updating state triggers a re-render of the component.
-
Suitable for form inputs when you need to display or react to user input in real-time.
-
useRef
: - Best for values that persist across renders but don't need to trigger re-renders when changed.
- Ideal for accessing DOM elements directly or storing mutable values like timers or previous state values.
- Prevents unnecessary re-renders, enhancing performance for certain use cases.
⚠️ Caveats and Pitfalls of Using useRef
While useRef
is a powerful tool in React, it's essential to use it judiciously to avoid common mistakes and potential issues. Here are some caveats and pitfalls to be aware of, along with examples to illustrate each point:
1. Bypassing React's Declarative Model
Danger
Using useRef
to manipulate DOM elements directly can interfere with React's declarative approach, making the code harder to understand and maintain.
Example: Inconsistent UI State
Imagine a scenario where you use useRef
to update an input's value directly, bypassing React's state management:
import React, { useRef } from 'react';
const InconsistentInput = () => {
const inputRef = useRef(null);
const updateInput = () => {
inputRef.current.value = "Updated via useRef!";
// React is unaware of this change
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={updateInput}>Update Input</button>
</div>
);
};
export default InconsistentInput;
In this example:
- Clicking the "Update Input" button changes the input's value directly.
- However, React remains unaware of this update since it's not using state.
- If other parts of the component rely on the input's value via state, inconsistencies can arise.
Solution
Prefer controlled components using useState
for form inputs and UI interactions unless there's a compelling reason to manipulate the DOM directly.
import React, { useState } from 'react';
const ControlledInput = () => {
const [value, setValue] = useState('');
const updateInput = () => {
setValue("Updated via useState!");
};
return (
<div>
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<button onClick={updateInput}>Update Input</button>
</div>
);
};
export default ControlledInput;
2. Potential for Stale or Inconsistent Data
Bug
Since updating a ref does not trigger a re-render, there's a risk of the UI displaying outdated information if the ref's value changes but the component doesn't re-render to reflect those changes.
Example: Stale Data in Event Handlers
Consider a counter using useRef
to track the current count without re-rendering:
import React, { useRef } from 'react';
const StaleCounter = () => {
const countRef = useRef(0);
const handleClick = () => {
countRef.current += 1;
console.log(`Current count: ${countRef.current}`);
};
return (
<div>
<p>Counter: {countRef.current}</p> {/* Always displays 0 */}
<button onClick={handleClick}>Increment</button>
</div>
);
};
export default StaleCounter;
In this example:
- Clicking "Increment" increases
countRef.current
and logs it. - However, the displayed counter (
countRef.current
) remains0
because the component doesn't re-render.
Solution
Use useState
for values that need to be reflected in the UI to ensure consistency.
import React, { useState } from 'react';
const PersistentCounter = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(prev => prev + 1);
console.log(`Current count: ${count + 1}`);
};
return (
<div>
<p>Counter: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
};
export default PersistentCounter;
3. Overusing Refs for State Management
Bug
Relying heavily on useRef
for managing state can lead to code that's difficult to debug and reason about, as it sidesteps React's state management patterns.
Example: Managing Multiple States with Refs
import React, { useRef } from 'react';
const OverusedRefs = () => {
const firstNameRef = useRef('');
const lastNameRef = useRef('');
const emailRef = useRef('');
const handleSubmit = () => {
console.log(`Name: ${firstNameRef.current} ${lastNameRef.current}`);
console.log(`Email: ${emailRef.current}`);
};
return (
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
<input type="text" placeholder="First Name" onChange={(e) => { firstNameRef.current = e.target.value; }} />
<input type="text" placeholder="Last Name" onChange={(e) => { lastNameRef.current = e.target.value; }} />
<input type="email" placeholder="Email" onChange={(e) => { emailRef.current = e.target.value; }} />
<button type="submit">Submit</button>
</form>
);
};
export default OverusedRefs;
In this example:
- Managing multiple form fields with
useRef
complicates the component. - It becomes harder to track changes, validate inputs, and manage stateful behaviors.
- Debugging becomes more challenging due to the lack of a centralized state.
Solution
Use useState
or form management libraries like Formik or React Hook Form for handling multiple form inputs efficiently.
import React, { useState } from 'react';
const ControlledForm = () => {
const [form, setForm] = useState({ firstName: '', lastName: '', email: '' });
const handleChange = (e) => {
const { name, value } = e.target;
setForm(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = () => {
console.log(`Name: ${form.firstName} ${form.lastName}`);
console.log(`Email: ${form.email}`);
};
return (
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
<input name="firstName" type="text" placeholder="First Name" value={form.firstName} onChange={handleChange} />
<input name="lastName" type="text" placeholder="Last Name" value={form.lastName} onChange={handleChange} />
<input name="email" type="email" placeholder="Email" value={form.email} onChange={handleChange} />
<button type="submit">Submit</button>
</form>
);
};
export default ControlledForm;
4. Memory Leaks with Unmanaged Refs
Bug
If refs are used to store subscriptions, event listeners, or instances of third-party libraries without proper cleanup, it can lead to memory leaks.
Example: Unmanaged Event Listener
import React, { useRef, useEffect } from 'react';
const MemoryLeakComponent = () => {
const buttonRef = useRef(null);
useEffect(() => {
const handleClick = () => console.log('Button clicked!');
const button = buttonRef.current;
button.addEventListener('click', handleClick);
// Missing cleanup
}, []);
return <button ref={buttonRef}>Click Me</button>;
};
export default MemoryLeakComponent;
In this example:
- An event listener is added to the button using
useRef
. - The
useEffect
hook does not return a cleanup function to remove the event listener. - If the component unmounts, the event listener remains, causing a memory leak.
Solution
Always clean up side effects in the useEffect
cleanup function when using refs for subscriptions or event listeners.
import React, { useRef, useEffect } from 'react';
const ManagedMemoryLeakComponent = () => {
const buttonRef = useRef(null);
useEffect(() => {
const handleClick = () => console.log('Button clicked!');
const button = buttonRef.current;
button.addEventListener('click', handleClick);
return () => {
button.removeEventListener('click', handleClick);
};
}, []);
return <button ref={buttonRef}>Click Me</button>;
};
export default ManagedMemoryLeakComponent;
5. Refs Not Updating Immediately
Bug
Updates to refs are synchronous, but accessing them immediately after setting can sometimes lead to unexpected behaviors, especially within event handlers.
Example: Immediate Access After Update
import React, { useRef } from 'react';
const ImmediateAccessRef = () => {
const inputRef = useRef('');
const handleChange = (e) => {
inputRef.current = e.target.value;
console.log(inputRef.current); // Always logs the latest value
};
const handleSubmit = () => {
// Trying to use inputRef.current here works fine
console.log(`Submitted value: ${inputRef.current}`);
};
return (
<div>
<input type="text" onChange={handleChange} />
<button onClick={handleSubmit}>Submit</button>
</div>
);
};
export default ImmediateAccessRef;
While this example works as expected, issues can arise in more complex scenarios where multiple updates happen in quick succession or within nested events, leading to potential inconsistencies.
Solution
To ensure the latest value is accessed, use the updated ref value within the next render cycle or within a useEffect
hook to guarantee the value is up-to-date.
6. Server-Side Rendering (SSR) Considerations
Bug
Accessing DOM elements via refs doesn't make sense in a server-side rendering context, as the DOM isn't available.
Example: Ref Access in SSR
import React, { useRef, useEffect } from 'react';
const SSRComponent = () => {
const divRef = useRef(null);
useEffect(() => {
// This will run only on the client, not on the server
if (divRef.current) {
console.log('Div dimensions:', divRef.current.getBoundingClientRect());
}
}, []);
return <div ref={divRef}>Hello, world!</div>;
};
export default SSRComponent;
In this example:
useRef
is used to access the div's dimensions.- During server-side rendering,
useEffect
doesn't run, so no errors occur. - However, attempting to access
divRef.current
outside ofuseEffect
could cause issues.
Solution
Ensure that any ref-based DOM manipulations occur within useEffect
or conditional checks that verify the presence of the DOM.
import React, { useRef, useEffect } from 'react';
const SafeSSRComponent = () => {
const divRef = useRef(null);
useEffect(() => {
if (typeof window !== 'undefined' && divRef.current) {
console.log('Div dimensions:', divRef.current.getBoundingClientRect());
}
}, []);
return <div ref={divRef}>Hello, world!</div>;
};
export default SafeSSRComponent;
📚 Best Practices
-
Use When Necessary
Prefer
useState
for managing form inputs unless there's a specific need to avoid re-renders. -
Avoid Overuse
Relying heavily on refs for state management can lead to code that's hard to maintain and debug.
-
Encapsulate Logic
For complex interactions, consider encapsulating ref-related logic within custom hooks to keep components clean.
-
Cleanup Effects
When using refs for side effects (like event listeners or third-party libraries), ensure you clean them up appropriately to prevent memory leaks.
-
Combine with
useEffect
When working with refs that interact with the DOM, use
useEffect
to handle side effects after the component has rendered.
📝 Summary
The useRef
hook is a powerful tool in React for managing mutable values and interacting directly with DOM elements without the overhead of state management. In the provided Login
component example, useRef
simplifies form handling by providing direct access to input values, enhancing both performance and code readability. However, it's crucial to use useRef
judiciously to avoid bypassing React's declarative patterns and to prevent potential issues like memory leaks or inconsistent UI states.
Understanding the caveats and pitfalls of useRef
, such as memory leaks and inconsistent UI states, is essential for building efficient and maintainable React applications. By adhering to best practices and being mindful of potential issues, you can leverage useRef
effectively without compromising the integrity of your components.