Search code examples
svelteastrojs

Why does the icon in this Astro/Svelte component flicker on refresh?


<script lang="ts">
  import { onMount } from "svelte";  
  let theme = localStorage.getItem('theme') ?? 'light';
  let flag = false;
  onMount(()=>{    
    flag = true
  })
  $: if (flag) {
    if ( theme === 'dark') {
      document.documentElement.classList.add("dark");
    } else {
      document.documentElement.classList.remove("dark");
    }     
    localStorage.setItem("theme", theme);
  }
  
  const handleClick = () => {    
    theme = (theme === "light" ? "dark" : "light");    
  }; 
</script>

<button on:click={handleClick}>{theme === "dark" ? "🌕" : "🌑"}</button>

the icon flickers when dark mode is enabled, in light mode this doesnt happen, I'm assuming this happens because its defaulting to lightmode when it initially renders, how can i fix this?


Solution

  • Problem specification

    The flicker is caused by an unexpected side effect of MPA (Multi Page Application). In SPA (Single Page Application), routing happens on client side and only a single page is fetched on startup, so the state of things such as menu or theme stay consistent on client side.

    Astro being an MPA would require a server page fetch when jumping from page to page. If the server does not know what the client set as theme (dark or light), it has to send a default one, then only when the <script> tag executes on the client, that's when the persisted state on client will kick in.

    It is possible to refer to this problem as server/client state synchronization

    To make the problem more spicy, we have to keep in mind that multiple clients could be using the website and not just one.

    Solutions

    For the solution purpose, if we think of the theme dark/light as being a counter 0/1

    SSG using client side routing

    I still want to mention this again in case of restriction for Static Site, then the two next solutions do not work, then fall back on client side routing is required => SPA

    next solutions apply for Server Side Rendering SSR

    SSR using cookies

    set by the client and read by the server upon request to ensure correct state is sent back singe page load

    here is the main code snippet on server side

    let counter = 0
    const cookie = Astro.cookies.get("counter")
    if(cookie?.value){
        counter = cookie.value
    }
    

    here the functions how to set and get the cookie on client side with vanialla js

    function get_counter(){
            const entry = document.cookie.split(';').find(entry=>entry.replace(' ','').startsWith('counter='))
            if(entry){
                return parseInt(entry.split('=')[1])
            }else{
                return 0
            }
        }
        function set_counter(counter){
            document.cookie = `counter=${counter}`
            console.log(`new counter value = ${counter}`)
        }
    

    SSR avoiding cookies with storage and url params

    Cookies are a much simpler solution, but in case this does not work for some reason (blocked, do not want notification,...) it is possible with storage and url parameters

    setting the url parameter on client side and managing storage

    ...
    window.history.replaceState(null, null, `?session_id=${session_id}`);
    ...
    let session_id = sessionStorage.getItem("session_id")
    ...
    sessionStorage.setItem("counter",counter)
    

    and this is how the server manages the session id

    let session_id = suid()
    if(Astro.url.searchParams.has('session_id')){
        session_id = Astro.url.searchParams.get('session_id')
        console.log(`index.astro> retrieved session_id from url param '${session_id}'`)
    }else{
        console.log(`index.astro> assigned new session_id '${session_id}'`)
    }
    

    References to full examples

    with cookies

    github : https://github.com/MicroWebStacks/astro-examples#13_client-cookie-counter

    Cookies did not got though some vm service provides, here a working one on Gitpod

    Gtipod : https://gitpod.io/?on=gitpod#https://github.com/MicroWebStacks/astro-examples/tree/main/13_client-cookie-counter

    without cookies with storage and url params

    github : https://github.com/MicroWebStacks/astro-examples#14_client-storage-counter

    This runs anywhere so here example on Stackblitz : https://stackblitz.com/github/MicroWebStacks/astro-examples/tree/main/14_client-storage-counter