← Back toMaster of React ref

Persist values between renders

Written byPhuoc Nguyen
Created
15 Oct, 2023
In our previous post, we learned how to use the `useRef()` hook to create a reference to an element in a functional component. But did you know that `useRef()` can also be used to keep a reference to any value using the same syntax?
One of the most useful applications of `useRef()` is to persist values between renders. This is especially helpful when you need to store data that doesn't need to trigger a re-render. For example, you might have a component that updates its state on every render, but there's one piece of data that doesn't need to cause a re-render. Or maybe you want to keep track of an input field value or a scroll position.
By using `useRef()`, you can track these values without causing unnecessary renders. The ref object returned by `useRef()` persists throughout the component's lifetime, even if it gets updated and causes a re-render.
In this post, we'll use the `useRef()` hook to build a real-world example that demonstrates this functionality. But before we get to that, let's first dive into the syntax of how to persist values.

Using useRef() to persist data

To persist data between renders using the `useRef()` hook, start by creating a variable and assigning it the initial value you want to keep. Then, in your component code, access the `current` property of the ref object to retrieve or update its current value.
For instance, let's say you create a reference called `counterRef`.
ts
const counterRef = React.useRef(0);
In this example, we create a reference with an initial value of zero. We can access the value stored in the reference via the `current` property, and we can also update its value by setting the `current` property.
Here's a code snippet that demonstrates how to increase or decrease the value of the reference:
ts
counterRef.current -= 1;
counterRef.current += 1;
By using this approach, you can effortlessly keep any value up-to-date across multiple renders without triggering unnecessary re-renders.

Building a drop indicator component

Now that you have a basic understanding of the `useRef()` hook for storing a value reference, let's use it to create a drop indicator component.
You've probably seen this component in action when dropping a file into an area, and an overlay appears on top of the drop target to indicate that you're dropping a file.
The drop indicator component is especially useful in file upload functionality. When a user wants to upload a file, they can drag and drop the file onto a designated area on the page. However, it's important to provide feedback to the user so they know where they can drop the file.
Another common usage for a drop indicator component is in drag and drop functionality for reordering items. For instance, if you have a list of items that can be reordered by dragging and dropping them into different positions, you might want to display a drop indicator to show where the item will be dropped when it's released.
In general, any time you have an interface that supports drag and drop interactions, there's likely an opportunity to use a drop indicator component to provide visual feedback to the user.
In this tutorial, we'll focus on building the `DropIndicator` component to serve the first use case. To start, we'll use the `useRef()` hook to create a reference to the root element.
tsx
const DropIndicator = () => {
const containerRef = React.useRef();

return (
<div
ref={containerRef}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
>
...
</div>
);
};
To determine if a user has dragged a file over our component, we need to handle four different events: `onDrop`, `onDragOver`, `onDragEnter`, and `onDragLeave`.
  • The `onDrop` event fires when the user drops a file onto the drop zone.
  • The `onDragOver` event fires continuously while an element is dragged over the drop zone.
  • The `onDragEnter` event fires when an element enters the drop zone during a drag operation.
  • The `onDragLeave` event fires when an element leaves the drop zone during a drag operation.
It's important to note that multiple events can trigger the `handleDragEnter` and `handleDragLeave` functions. For example, if a user drags a file over the drop zone, hovers over another element on the page, and then returns to the drop zone, this will trigger multiple `handleDragEnter` and `handleDragLeave` events.
To keep track of how many times the drag event has been triggered, we'll use the `useRef()` function again.
tsx
const dragCount = React.useRef(0);
const [isDragging, setDragging] = React.useState(false);
In the sample code, we use an internal state called `isDragging` to check if users are dragging a file.
When a user drags a file over the component, the `handleDragEnter` event handler is triggered. This function increments the value stored in `dragCount.current`. If this is the first time that the drag event has occurred, then `setDragging(true)` is called, which updates the state variable `isDragging` to `true`.
This is how we handle the `onDragEnter` event:
tsx
const handleDragEnter = (e: DragEvent): void => {
e.preventDefault();
dragCount.current += 1;
if (dragCount.current <= 1) {
setDragging(true);
}
};
When a user drags a file off of the component, it triggers the `handleDragLeave` event handler. This function reduces the value stored in `dragCount.current`. If the drag event has occurred for the last time (i.e., if `dragCount.current` is zero), then `setDragging(false)` is called, which updates the `isDragging` state variable to `false`.
tsx
const handleDragLeave = (): void => {
dragCount.current -= 1;
if (dragCount.current <= 0) {
setDragging(false);
}
};
Using the `dragCount` reference helps us keep track of how many times the drag event has been triggered. When this count reaches zero, we know that no more elements are being dragged over our component, so we can update the state variable accordingly. This ensures that we only show the drop indicator when necessary and hide it when not needed.
When the user drops a file onto the component, it triggers the `handleDrop` event handler. This function receives a `DragEvent` object as its argument, which contains information about the dropped item. This way, we can handle the dropped item and perform any necessary actions.
tsx
const handleDrop = (e: DragEvent): void => {
e.preventDefault();
dragCount.current = 0;
setDragging(false);
const files = e.dataTransfer.files;

// Do something with files ...
};
When a file is dropped onto our component, we use `preventDefault()` to stop the browser's default behavior. We also set `dragCount.current` to zero and update `isDragging` to false since no more elements are being dragged over our component.
The `e.dataTransfer.files` property contains an array of File objects that represent the files that were dropped. You can use this information to do whatever you need to do with those files, such as uploading them to a server or processing them in some way.
For example, you could upload each file using `fetch`, like this:
tsx
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.append('file', files[i]);
}

try {
const response = await fetch('/upload', {
method: 'POST',
body: formData,
});
console.log(response.json());
} catch(error) {
console.error(error);
}
Here's an example of how to handle files that are dropped onto your component. First, we create a new `FormData` object and add each file to it using a loop. Then, we use `fetch()` to send a POST request to the server with the `FormData` object in the request body. Once the server processes the files, it sends a response back to the client, which we can log to the console using `response.json()`. If there's an error, we catch it and log it to the console.
This is just one example of what you can do with the files dropped onto your component. The possibilities are endless!
Finally, we need to make sure we call `preventDefault()` inside the `handleDragOver` function. This will stop the browser from opening or displaying the dragged file, which is not what we want. Instead, we want to provide visual feedback to the user that they're dragging a file over a valid drop target. If we don't call `preventDefault()`, the browser's default behavior will interfere with our custom drag and drop behavior, potentially causing issues with our component's functionality.
tsx
const handleDragOver = (e: DragEvent): void => {
e.preventDefault();
};
While `isDragging` is true, the component will display a message prompting the user to drop their file onto it.
tsx
{
isDragging ? (
"Drag and drop a file here"
) : (
<button>Upload a file</button>
)
}

Demo

Give it a shot: try dragging and dropping a file onto the demo below. Note that the main button is only for demonstration purposes and won't actually open a dialog box for you to choose a file.
If you found this post helpful, please consider giving the repository a star on GitHub or sharing the post on your favorite social networks 😍. Your support would mean a lot to me!

Questions? 🙋

Do you have any questions about front-end development? If so, feel free to create a new issue on GitHub using the button below. I'm happy to help with any topic you'd like to learn more about, even beyond what's covered in this post.
While I have a long list of upcoming topics, I'm always eager to prioritize your questions and ideas for future content. Let's learn and grow together! Sharing knowledge is the best way to elevate ourselves 🥷.
Ask me questions

Recent posts ⚡

Newsletter 🔔

If you're into front-end technologies and you want to see more of the content I'm creating, then you might want to consider subscribing to my newsletter.
By subscribing, you'll be the first to know about new articles, products, and exclusive promotions.
Don't worry, I won't spam you. And if you ever change your mind, you can unsubscribe at any time.
Phước Nguyễn