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
.
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:
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.
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.
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:
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
.
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.
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:
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.
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.
{
isDragging ? (
"Drag and drop a file here"
) : (
<button>Upload a file</button>
)
}
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.
It's highly recommended that you visit the original post to play with the interactive demos.
If you found this series 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!
If you want more helpful content like this, feel free to follow me:
Top comments (0)