Preload initial menu and store into localStorage?

I have noticed that the “hen and egg” problem will occur now and then using Javascript. If I load a stored menu by fetch, I often cannot use this menu until it is “finished”. And async will make it more unpredictable as the load time is different.

My intention is to fetch menus from database and then serve them dynamically from the menus in localStorage. But I must be sure that localStore is ready to use.

Is it possible to preload data ONCE and use this stored data in a predictable way?

async function get_menu() {
  let url = "https://api3.go4webdev.org/main/std";
  await fetch(url)
    .then(response => response.json())
    .then(data => localStorage.setItem('main', data))
}

get_menu()
alert(localStorage.main) <--- Not loaded first time in a new browser...

check to see if localStorage contains an item called main. If it does, call make_menu. If not, call get_menu, which should, as part of its then, call make_menu.
Notably, this isnt dynamic. It’s a single prefetch.

2 Likes

I don’t get why you can’t fetch your menu each time.

If I look at your jsFiddle there is an issue that stands out to me.

get_menu()
alert(localStorage.main)

alert is being called as if get_menu is synchronous. You could instead use a thenable for a more predicatable output.

get_menu().then(() => alert(localStorage.getItem(main)))

You are also using thenables inside of an async/await function. It works, but you could use await instead.

async function set_menu() {
  try {
    const response = await fetch("https://api3.go4webdev.org/main/std")
    const data = await response.json()
    localStorage.setItem('main', JSON.stringify(data))
  } catch (err) {
    console.error(err)
  }
}

I could be wrong, but this indicates to me that maybe you need to look at your code to try and fix the ‘hen and egg’ issue.

3 Likes

This.

If your database connection is that slow that it’s noticable, you’ve got other issues.
If it’s not, load every time.

You could try a middle-ground approach (“timed cache”), wherein you store the menu for a certain TTL; if they need the menu again before TTL, serve from localstorage, else get new (and set new TTL).

2 Likes

It is about philosophy.

Do not repeat yourself (DRY) if possible. Leave as small footprint as possible to the environment (less traffic, less CPU, less memory etc). Make it as fast as possible (memory is about the fastest). Less code used to fetch from localStorage than fetch from database. Etc…

Load the menu for each page x 1000 users may cause a lot of traffic, or?

Maybe I got it wrong, but this is my way of thinking.

Thank you! If I interpret you correctly, is this predictable enough?

async function get_menu() {
  try {
    let response = await fetch("https://api3.go4webdev.org/main/std")
    let data = await response.json()
    localStorage.setItem('main', data)
  } catch (err) {
    console.error(err)
  }
}

get_menu()
alert(localStorage.main)

So leaving your menus laying around in peoples’ computers who may not visit your site again is… less of a footprint?

Thinking of sessionStorage in production :slight_smile:

I believe that only works for a specific tab. Not thoroughly tested, but maybe firing a cleanup using window.beforeunload.

No you are missing some points there.

One data should be stored as a string using JSON.stringify

localStorage.setItem('main', JSON.stringify(data))

Then parse the retrieved data, maybe something along the lines of

const menu = localStorage.getItem('main') // returns null if no 'main' in storage
return (menu !== null)
  ? JSON.parse(menu) // parse the JSON to JS objects
  : fetchMainMenu() // fetch from the DB

Second, this

get_menu()
alert(localStorage.main)

get_menu is a non-blocking asynchronous function that returns a promise. What that means is the rest of the code e.g. alert(localStorage.main) will be executed before the returned promise is fullfilled.

By using a thenable, you can execute a callback once the promise is fullfilled — simple speak, once get_menu has finished then execute a callback.

So this instead

get_menu() // async functions return a promise
  .then(() => {
     const menu = localStorage.getItem('main')
     // just for a quick test
     if (menu !== null) {
       console.log(JSON.parse(menu))
     } else {
       console.log('No menu found!')
     } 
  })

// or if inside another function

const anotherFunction = async () => {
  await get_menu()
  const menu = localStorage.getItem('main')
  ...
}

edit: Just a small point
Use const instead of let

const response = await fetch("https://api3.go4webdev.org/main/std")
const data = await response.json()

let for values that are going to change e.g. let count = 0; count += 2

3 Likes

Thank you for your detailed explanation. A few of this I have figured out myself, but most where new to me.

1 Like

According to my tests, it works for the whole session until you close the window. But I will test further to confirm this. But switching tab is sort of “close” to me.

I do not get this to work. But I know I should do this in this way.

TypeError: undefined is not an object (evaluating ‘get_menu() // async functions return a promise
.then’)"

I get this error: “TypeError: undefined is not an object (evaluating 'get_menu() // async functions return a promise”

But this works - the second time :slight_smile:

https://jsfiddle.net/62quhvk5/

In none of those jsfiddles you have posted is get_menu an async function.

function get_menu() {
  let url = "https://api3.go4webdev.org/main/std";
  fetch(url)
    .then(response => response.json())
    .then(data => {
      sessionStorage.setItem('main', JSON.stringify(data));
    });
}

There is no return specified in that function, so undefined is returned, so this is not going to work.

undefined.then(() ... 

The example I posted with async/await in post #3, should work. Do you have a fiddle with that approach?

Still not working the first time. No eggs :slight_smile:

load_menu should be an async function too.

const load_menu = async () => {
  try {
    const response = await fetch('https://api3.go4webdev.org/main/std')
    const data = await response.json()
    sessionStorage.setItem('main', JSON.stringify(data));
  } catch (err) {
    console.error(err)
  }
}

I have to go to work now, so won’t be about for a bit.

I think this work now. I have a sessionStore.clear() to ensure that it really works. Commenting this line reduce the flickering a lot (loading from sessionStore instead)

Thank You indeed for your patience!

1 Like

Updated jsfiddle (API changed): https://jsfiddle.net/wz1qjybu/

1 Like

I was having a bit of a play with your jsfiddle @sibertius

Actually couldn’t figure out why sessionStorage wasn’t working until I spotted.

sessionStorage.clear() //to ensure that it work (remove)

I tested your code using vscode and liveserver, and ran into a problem. It seems to me checking for the length on sessionStorage could potentially be problematic. In the case of liveserver it adds its own entry to sessionStorage, so in my tests the check against length told me I had a stored menu, when I didn’t.

If you can guarantee no other scripts in your page are going to use sessionStorage, then fine. Otherwise it might be safer to actually check for the ‘main’ property instead.

Just for your consideration :slight_smile:

const load_menu = async () => {
  try {
    const response = await fetch('https://api3.go4webdev.org/menu/std')
    const menu = await response.json()

    sessionStorage.setItem('main', JSON.stringify(menu))
    return menu // Why not return the menu?
  } catch (err) {
    console.error(err)
  }
}

const get_menu = async () => {
  // start by trying to get the menu from storage
  const storedMenu = sessionStorage.getItem('main')

  // if storedMenu is null 
  // ? assign to menu the returned menu from load_menu()
  // : otherwise assign the parsed storedMenu
  const menu = (storedMenu === null)
    ? await load_menu()
    : JSON.parse(storedMenu)

  fillmenu(menu)
}

I agree. Should fix this! Thanks for your feedback.

1 Like