4 - Saving Changes
4.2 Input Changes

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.

NoteCard.jsx
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:

  1. First, we initiate our saving state. This will be used later to render a "saving" indicator.
  2. 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.
  3. Finally, we call setTimeout and tell it to call the saveData method 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

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>
)}