DEV Community

Cover image for Persist values between renders
Phuoc Nguyen
Phuoc Nguyen

Posted on • Edited on • Originally published at phuoc.ng

Persist values between renders

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);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
    );
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
    }
};
Enter fullscreen mode Exit fullscreen mode

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);
    }
};
Enter fullscreen mode Exit fullscreen mode

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 ...
};
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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();
};
Enter fullscreen mode Exit fullscreen mode

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

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)