Input Changes
Before we update our note body lets consider the situation we are in. We want our note to be updated automaticly without explicityly clicking a save button, but we don’t want to make a request to our database on every “keyup” event.
The solution I decided on is to update the note every two seconds after the key-up event gets triggered. However, if there are multiple key-up events, the timer will keep extending the two-second countdown until the last key-up event. So only when a user has completly stopped typing for 2 seconds we will make a request to update this note in our database.
Saving State with Timer
We'll start by adding a saving state to our note in order to indicate if our note is in the process of saving data.
const [saving, setSaving] = useState(false);We'll use a ref to hold the timer id when a user initiates a key up event, so for this we will set a ref called keyUpTimer
const keyUpTimer = useRef(null);Now what we'll do is create a function that handles the keyup event.
const handleKeyUp = async () => {
    //1 - Initiate "saving" state
    setSaving(true);
 
    //2 - If we have a timer id, clear it so we can add another two seconds
    if (keyUpTimer.current) {
        clearTimeout(keyUpTimer.current);
    }
 
    //3 - Set timer to trigger save in 2 seconds
    keyUpTimer.current = setTimeout(() => {
        saveData("body", textAreaRef.current.value);
    }, 2000);
};Let's review what we just did here:
- First, we initiate our saving state. This will be used later to render a "saving" indicator.
- Next, we check if there is a current timer ID. If there is, we need to clear it so we can set a new one, therefor adding another 2 seconds before we call the save method.
- Finally, we call setTimeoutand tell it to call thesaveDatamethod in 2000 milliseconds (2 seconds)
Add key up event to textAreaRef
<textarea
    onKeyUp={handleKeyUp}To set our "saving" state back to false, we'll call at the end of the saveData method, so we can later indicate all is complete.
const saveData = async (key, value) => {
    const payload = { [key]: JSON.stringify(value) };
    try {
        await db.notes.update(note.$id, payload);
    } catch (error) {
        console.error(error);
    }
    setSaving(false);
};Render "saving" indicator
Within our card-header we can now use the saving state to render out a message that indicates the note is in the process of being saved/updated.
{
    saving && (
        <div className="card-saving">
            <span style={{ color: colors.colorText }}>Saving...</span>
        </div>
    );
}Load spinner
Create a Spinner icon and add css: src/icons/Spinner.jsx
const Spinner = ({ color = "#fff", size = "20" }) => {
    return (
        <svg
            className="spinner"
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 24 24"
            width={size}
            height={size}
            stroke={color}
            fill="none"
            strokeWidth="1.5"
        >
            <path
                strokeLinecap="round"
                strokeLinejoin="round"
                d="M12 20c-4.416 0-8-3.584-8-8s4.448-7.112 4.448-7.112m0 0v3.616m0-3.616h-4M12 4c4.416 0 8 3.552 8 8 0 5.336-4.448 8-4.448 8m0 0h4m-4 0v-3.552"
            ></path>
        </svg>
    );
};
 
export default Spinner;Add the following CSS for our load spinner
:root {
    /* ... */
    --spinner-animation-speed: 2s;
}
 
@keyframes spin {
    from {
        transform: rotate(0deg);
    }
    to {
        transform: rotate(360deg);
    }
}
 
.spinner {
    animation: spin var(--spinner-animation-speed) linear infinite;
}
 
.card-saving {
    display: flex;
    align-items: center;
    gap: 5px;
}Add the load spinner to the header of our NoteCard
{saving && (
    <div className="card-saving">
        <Spinner color={colors.colorText} />
        <span style={{ color: colors.colorText }}>
            Saving...
        </span>
    </div>
)}