USING LOCALSTORAGE TO PERSIST UNSAVED SESSIONS IN MY NUXT APP

Using localStorage to persist unsaved sessions in my Nuxt app

April 29, 2024 . Mostafa Lotfy

Hey, I'm building my indie apps and sharing some of the lessons I come across on my website.

OneExercise Workout Tracker

I finally built my first App: OneExercise (1Xercise): A free workout tracker that runs in your browser and gives you instant colored feedback by comparing the current workout to the very last one. I am still fixing some bugs and rethinking the design and functionality to improve the UX.

Screenshot showing OneExercise (1Xercise) app's side menu with the current version Beta

But, it's very functional. I've been using it for the past 3+ months to track my own workouts.

Screenshot showing 1Xercise app's stats page, and showing the period I've been using the app to track my workouts (period: 113 days)

In the above video, I talk about one of the latest issues I found while using my NuxtJs app, OneExercise (1Xercise), and how I used localStorage to fix it.

Here are some quick notes from the video:

The Problem: Losing unsaved session state on page reloads and app closures

I came across an issue while using OneExercise, where I was losing my running (unsaved) workout session whenever the page is reloaded or the app is closed. This was especially painful whenever I was tracking a workout that has a timer (like a 'Dead Hang' or a 'Power walk' workout)

I mainly use OneExercise as a PWA downloaded by the Chrome browser on my Android phone. Whenever I switch to another app temporarily, the Chrome browser tends to restart the app, resulting in a very bad experience and losing unsaved progress.

Different ways to save data in the browser

There are many ways to save data in the browser. (This article shows 10 different ways to save data in the browser.)

I had already been using indexedDb to store OneExercise users' workouts & exercises in the browser via Dexie.Js. Dexie.Js is a wrapper library that simplifies working with indexedDb that I use in OneExercise. However, indexedDb is more suitable for storing a large amount of structured data and is an overkill for what I needed to do, which is to temporarily store a very small amount of data (the current user's unsaved workout info), in the browser.

I looked into sessionStorage, but sessionStorage would only save state across page reloads, if the app is closed the data will be lost.

I found localStorage to be the most suitable solution; it stores state across page reloads and even when the app is closed.

How To Use localStorage

The following code example shows how simple it is to use localStorage to quickly save state in the browser.

// Saving each item separately in localStorage as a string
 
let workoutName = 'Sprints'
 
localStorage.setItem('unsavedWorkoutName', workoutName)
workoutName = localStorage.getItem('unsavedWorkoutName')
 
// clean up
localStorage.removeItem('unsavedWorkoutName')
 
// Saving related items as one object in localStorage
 
let workout = {
	name: 'Walk',
	date: new Date()
}
 
localStorage.setItem('unsavedWorkout', JSON.stringify(workout))
 
// update workout val
workout = JSON.parse(localStorage.getItem('unsavedWorkout'))
 
// clean up
localStorage.removeItem('unsavedWorkout')

How I used localStorage in my Nuxt.js app 1Xercise

I created this simple GitHub repo to show how I persist unsaved workout sessions in OneExercise.

When a user adds/edits a workout, I watch the values of the current unsaved workout session, if they are changed, I save the changes into localStorage.

<script setup lang="ts">
// ...
 
// Save to localStorage
watch([workoutName, workoutDate], ([newName, newDate], [oldName, oldDate]) => {
			localStorage.setItem('unsavedWorkoutName', newName)
			localStorage.setItem('unsavedWorkoutDate', newDate)
			localStorage.setItem('unsavedId', workoutId as string)
})
 
// ...
</script>

Then, whenever the page is reloaded, I check if there is an unsaved session in localStorage, if I find one I load it.

 
<script lang="ts" setup>
 
	// ...
 
  onMounted(() => {
    // Load from localStorage
    const unsavedId = localStorage.getItem('unsavedId') ?? ''
    const unsavedName = localStorage.getItem('unsavedWorkoutName') ?? ''
    const unsavedDate = localStorage.getItem('unsavedWorkoutDate') ?? ''
    if (unsavedId) {
        workoutName.value = unsavedName
        workoutDate.value = unsavedDate
    }
  })  
 
	// ...
</script>

Finally, when the user presses navigates back, I remove the workout session from localStorage:

<script lang="ts" setup>
 
	// ...
 
  onBeforeRouteLeave(() => {
    // Reset localStorage
    localStorage.removeItem('unsavedId')
    localStorage.removeItem('unsavedWorkoutName')
    localStorage.removeItem('unsavedWorkoutDate')
  })
</script>

If the app is closed, the user will first visit the root page. So I check if there is already an unsaved running workout session in localStorage, if yes I direct the user towards it using the ID I saved in localStorage, if not I just direct the user to the workout page as the normal entry point of the user journey.

<script lang="ts" setup>
  const router = useRouter();
 
  onMounted(() => {
    const unsavedId = localStorage.getItem('unsavedId') ?? ''
 
    if (unsavedId) {
      router.push(`/workouts/${unsavedId}`);
    } else {
      router.push('/workouts')
    }
  })
</script>

Checkout the full code at the GitHub repo, or the video for more details.

Links from this post

Other Posts You Might Find Interesting

#nuxtjs #localstorage #1xercise #oneexercise #indiedev #mstflotfy #webdevelopment