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
setTimeout
and tell it to call thesaveData
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
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>
)}