S
SolidJSโ€ข2y ago
Trader101

Flickering UI with solidstart

When I am on dark mode with tailwindcss and I navigate between routes for the first time, the theme is for a moment light before going dark. This is very bad for user experience. issue happens when: - first load - reloading page - navigating between routes for 1st time
60 Replies
Trader101
Trader101OPโ€ข2y ago
component that changes theme (placed in header which is fixed and visible outside routes, directly placed in root.jsx).
const [darkMode, setDarkMode] = createSignal(
JSON.parse(localStorage.getItem("dark")) ?? false
);

createEffect(() => {
localStorage.setItem("dark", JSON.stringify(darkMode()));
if (darkMode()) {
document.documentElement.classList.remove("dark");
} else {
document.documentElement.classList.add("dark");
}
});

return <button onClick={() => setDarkMode(prev => !prev)}> swicth </button>
const [darkMode, setDarkMode] = createSignal(
JSON.parse(localStorage.getItem("dark")) ?? false
);

createEffect(() => {
localStorage.setItem("dark", JSON.stringify(darkMode()));
if (darkMode()) {
document.documentElement.classList.remove("dark");
} else {
document.documentElement.classList.add("dark");
}
});

return <button onClick={() => setDarkMode(prev => !prev)}> swicth </button>
and for example about route
import { Title } from "solid-start";

export default function About() {
return (
<main class="dark:bg-[#0d1117] grow">
<Title>About | Todesign </Title>
</main>
);
}
import { Title } from "solid-start";

export default function About() {
return (
<main class="dark:bg-[#0d1117] grow">
<Title>About | Todesign </Title>
</main>
);
}
@davedbase
davedbase
davedbaseโ€ข2y ago
Responding to your last comment on the solid-start channel: part of the issue is that localStorage doesn't exist on the server when you first visit the application. localStorage is a browser API So you'll have to guard access to it on first load:
const [darkMode, setDarkMode] = createSignal(
localStorage ? JSON.parse(localStorage.getItem('dark')) : false
);
const [darkMode, setDarkMode] = createSignal(
localStorage ? JSON.parse(localStorage.getItem('dark')) : false
);
It would be better to store the dark mode value in a cookie because then the server would have access to it and could SSR the dark mode property on load.
Trader101
Trader101OPโ€ข2y ago
Ok thanks I am using ssr false so for me everything works good
davedbase
davedbaseโ€ข2y ago
Yeah. ๐Ÿ™‚ I'm glad!
Trader101
Trader101OPโ€ข2y ago
but the UI flickering any idea why it is happening ?
davedbase
davedbaseโ€ข2y ago
On first load?
Trader101
Trader101OPโ€ข2y ago
issue happens when: - first load - reloading page - navigating between routes for 1st time
davedbase
davedbaseโ€ข2y ago
It's the same issue, the page always starts with a default of not having dark mode. You just used ssr: false to disable the server-side rendering to fix localStorage. The proper way to solve this is store the dark mode state in a cookie and SSR the value on load.
Trader101
Trader101OPโ€ข2y ago
the ssr false was already set up before my question. I was referring to stackblitz but I need also window.etherum
davedbase
davedbaseโ€ข2y ago
You could enable SSR and check if window exists before accessing that property. In essence the flicker you're seeing isn't a Start issue itself, it's an issue with being able to properly store the value in a safe place that can be SSRd so that upon initial load the value is already set. You'll find the flicker is an issue for any SPA (which is what you are basically serving up when you use ssr: false)
Trader101
Trader101OPโ€ข2y ago
you mean a spa with routes ?
davedbase
davedbaseโ€ข2y ago
Any SPA, really.
Trader101
Trader101OPโ€ข2y ago
well here on one of my sites I did some time ago it doesn't flicker https://rattlessnake.github.io/Factorization-Calculator/ using react try setting dark mode and reloading
davedbase
davedbaseโ€ข2y ago
Well, it's actually still happening but the load time is masking it here. If you change your Network tab to Slow 3G throttle you can see the flicker.
davedbase
davedbaseโ€ข2y ago
The white is the gap between the first paint and the JS being resolved.
Trader101
Trader101OPโ€ข2y ago
yes but the white is on the entire page, I believe this happens frequently on low internet connection. My flickering only happens on the main, not the header (which is directly in root.jsx) yes absolutely just tried out with my app with low end mobile. So the page is white then content resolves (header + main) but the main is still white for 2 seconds
davedbase
davedbaseโ€ข2y ago
Sorry, I think we may be on different pages in regards to what the issue is. It would really help if you provided me with a reproduction of your issue so I can support more clearly. If others have a better understanding please chime in. I might be thinking of this wrong haha
Trader101
Trader101OPโ€ข2y ago
yeah sorry I can't show you my code. But I will give you more info one sec
davedbase
davedbaseโ€ข2y ago
That's fine, maybe just distill the issue down to the mechanics in a reproduction. Like basically recreate the issue in a simple form via Stackblitz.
Trader101
Trader101OPโ€ข2y ago
Trader101
Trader101OPโ€ข2y ago
header is in root.jsx
import {
Body,
FileRoutes,
Head,
Html,
Meta,
Link,
Routes,
Scripts,
Title,
} from "solid-start";
import "./root.css";
import Header from "./components/header";

export default function Root() {
return (
<Html lang="en">
<Head>
<Title>Hi</Title>
<Meta charset="utf-8" />
<Meta name="viewport" content="width=device-width, initial-scale=1" />
<Link type="image/png" rel="icon" href="images/i.png" />
</Head>
<Body>
<Header />
<Routes>
<FileRoutes />
</Routes>
<Scripts />
</Body>
</Html>
);
}
import {
Body,
FileRoutes,
Head,
Html,
Meta,
Link,
Routes,
Scripts,
Title,
} from "solid-start";
import "./root.css";
import Header from "./components/header";

export default function Root() {
return (
<Html lang="en">
<Head>
<Title>Hi</Title>
<Meta charset="utf-8" />
<Meta name="viewport" content="width=device-width, initial-scale=1" />
<Link type="image/png" rel="icon" href="images/i.png" />
</Head>
<Body>
<Header />
<Routes>
<FileRoutes />
</Routes>
<Scripts />
</Body>
</Html>
);
}
topmenu is where you change theme topmenu
export default function TopMenu() {
const [darkMode, setDarkMode] = createSignal(
JSON.parse(localStorage.getItem("dark")) ?? false
);

createEffect(() => {
localStorage.setItem("dark", JSON.stringify(darkMode()));
if (darkMode()) {
document.documentElement.classList.remove("dark");
} else {
document.documentElement.classList.add("dark");
}
});

return (
<section>
<div class="flex gap-2">
<button onClick={() => setDarkMode(prev => !prev)}
>
{darkMode() ? <LightModeIcon /> : <DarkModeIcon />}
</button>

</div>
</section>
);
}
export default function TopMenu() {
const [darkMode, setDarkMode] = createSignal(
JSON.parse(localStorage.getItem("dark")) ?? false
);

createEffect(() => {
localStorage.setItem("dark", JSON.stringify(darkMode()));
if (darkMode()) {
document.documentElement.classList.remove("dark");
} else {
document.documentElement.classList.add("dark");
}
});

return (
<section>
<div class="flex gap-2">
<button onClick={() => setDarkMode(prev => !prev)}
>
{darkMode() ? <LightModeIcon /> : <DarkModeIcon />}
</button>

</div>
</section>
);
}
@davedbase I mean that is basically it
davedbase
davedbaseโ€ข2y ago
Where is TopMenu inserted into the application? Is it composed in Header?
Trader101
Trader101OPโ€ข2y ago
yes index.jsx of header root.css if you want
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
.themed-text {
@apply text-violet-500;
}
.themed-bg {
@apply bg-violet-500;
}
.themed-border {
@apply border-violet-500;
}
.themed-gradient-text {
@apply text-transparent bg-clip-text bg-gradient-to-r from-violet-600 to-purple-500;
}
.subtext {
@apply text-slate-400;
}
}

@font-face {
font-family: "Roboto Slab";
src: url(fonts/roboto-slab.ttf);
}
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
.themed-text {
@apply text-violet-500;
}
.themed-bg {
@apply bg-violet-500;
}
.themed-border {
@apply border-violet-500;
}
.themed-gradient-text {
@apply text-transparent bg-clip-text bg-gradient-to-r from-violet-600 to-purple-500;
}
.subtext {
@apply text-slate-400;
}
}

@font-face {
font-family: "Roboto Slab";
src: url(fonts/roboto-slab.ttf);
}
you need to know that a default web page is white until some CSS tells it otherwise createEffect will run once the component is mounted, not before so you have default web page (white) -> component mounts -> effect runs and turns it dark
@numnumberry what should I do then ?
sabercoy
sabercoyโ€ข2y ago
ile show you, let me get some code
sabercoy
sabercoyโ€ข2y ago
sabercoy
sabercoyโ€ข2y ago
basically it is implementing what he said here
davedbase
davedbaseโ€ข2y ago
Here's an example: https://stackblitz.com/edit/github-697vjw?file=src/routes/about.tsx of what you're currently doing right?
Trader101
Trader101OPโ€ข2y ago
so ssr is true now ? my app gets broken when I enable ssr errors poping everywhere lol
sabercoy
sabercoyโ€ข2y ago
that may be due to unrelated code you got going on lol
davedbase
davedbaseโ€ข2y ago
As mentioned because the server doesn't have a localStorage object ๐Ÿ™‚ You have to guard against accessing that with isServer or checking if it exists
Trader101
Trader101OPโ€ข2y ago
yes and how to access window.etherum ?
davedbase
davedbaseโ€ข2y ago
You cannot from the server
Trader101
Trader101OPโ€ข2y ago
then how ?
sabercoy
sabercoyโ€ข2y ago
you would need to wait until it gets to and runs in the client and call it with createEffect()
Trader101
Trader101OPโ€ข2y ago
please kindly show me on here on to implement this
import { createSignal, onMount } from "solid-js";
import { useNavigate } from "solid-start";
import shorten from "../../utils/shorten";

export default function ConnectWallet() {
const [address, setAddress] = createSignal("");
const navigate = useNavigate();

onMount(() => setAddress(window.ethereum?.selectedAddress || ""));

const connect = async () => {
if (window.ethereum) {
try {
const [selectedAddress] = await window.ethereum.request({
method: "eth_requestAccounts",
});
setAddress(selectedAddress);
navigate(selectedAddress);
} catch {
alert("Connection rejected");
}
} else {
alert("No wallet found");
}
};

return (
<Show
when={address()}
fallback={
<button onClick={() => connect()}>Connect</button>
}
>
<button
onClick={() => navigate(address())}
>
{shorten(address())}
</button>
</Show>
);
}
import { createSignal, onMount } from "solid-js";
import { useNavigate } from "solid-start";
import shorten from "../../utils/shorten";

export default function ConnectWallet() {
const [address, setAddress] = createSignal("");
const navigate = useNavigate();

onMount(() => setAddress(window.ethereum?.selectedAddress || ""));

const connect = async () => {
if (window.ethereum) {
try {
const [selectedAddress] = await window.ethereum.request({
method: "eth_requestAccounts",
});
setAddress(selectedAddress);
navigate(selectedAddress);
} catch {
alert("Connection rejected");
}
} else {
alert("No wallet found");
}
};

return (
<Show
when={address()}
fallback={
<button onClick={() => connect()}>Connect</button>
}
>
<button
onClick={() => navigate(address())}
>
{shorten(address())}
</button>
</Show>
);
}
sabercoy
sabercoyโ€ข2y ago
I dont know the entirety of your code or what you are trying to do but to stop the CSS flickering the idea is to set the CSS on the server before it ever gets to the client NOT to set it on the client using createEffect, because by then its slightly too late and you see the white flicker
davedbase
davedbaseโ€ข2y ago
That should work in SSR. onMount isn't called on the server. Also nothing happens until you click. Are you having issues with this though? If so what specifically? Also this seems a departure from the original issue hehe
davedbase
davedbaseโ€ข2y ago
In regards to the dark mode concern, here's an example of my reproduction working as expected....
davedbase
davedbaseโ€ข2y ago
Does yours not operate that way?
Trader101
Trader101OPโ€ข2y ago
try going dark then reload
davedbase
davedbaseโ€ข2y ago
Yeah you'll see white then the page will load.
Trader101
Trader101OPโ€ข2y ago
no only main is white...
davedbase
davedbaseโ€ข2y ago
Likely because createEffect is calling after mount and adding dark to the header. So the sequence of events is: - Page loads - JS is loaded page is painted - Route loads and component is mounted and rendered - createEffect kicks in after mount and makes dark
Trader101
Trader101OPโ€ข2y ago
ok now ssr works ok guys thanks I'll fix this tomorrow kinda late here ๐Ÿ™‚ I'll do the cookie thing
davedbase
davedbaseโ€ข2y ago
There are some super helpful primitives to make your life easier by the way ๐Ÿ™‚ https://github.com/solidjs-community/solid-primitives/tree/main/packages/media#useprefersdark
GitHub
solid-primitives/packages/media at main ยท solidjs-community/solid-p...
A library of high-quality primitives that extend SolidJS reactivity. - solid-primitives/packages/media at main ยท solidjs-community/solid-primitives
davedbase
davedbaseโ€ข2y ago
This is definitely resolvable though. I'm a bit out of time but it would be wonderful if someone could factor an example that used cookies + the above primitive to demonstrate how to solve this with SSR. I'm sure our friend @rattlessnake would really appreciate it!
Trader101
Trader101OPโ€ข2y ago
Hi, just few things before I close this post. Now that I use ssr, is this the correct way to access window?
const [useradr, setUseradr] = createSignal("");
onMount(() => setUseradr(window.ethereum?.selectedAddress ?? ""));
const [useradr, setUseradr] = createSignal("");
onMount(() => setUseradr(window.ethereum?.selectedAddress ?? ""));
I have to go through state everytime ? is it good pratice to use ErrorBoundary ?
sabercoy
sabercoyโ€ข2y ago
you should access window inside a createEffect (or onMount, which is a createEffect under the hood) I guess you can also access window inside a check for if (!isServer) {} I would say its good practice at the top level, it will catch errors you do not catch yourself
Trader101
Trader101OPโ€ข2y ago
should I wrap errorBoundary only on routes or on entire body ?
<Body>
<Header />
<ErrorBoundary>
<Routes>
<FileRoutes />
</Routes>
</ErrorBoundary>
<Scripts />
</Body>
<Body>
<Header />
<ErrorBoundary>
<Routes>
<FileRoutes />
</Routes>
</ErrorBoundary>
<Scripts />
</Body>
I mean when there is an error in routes my entire app still crashes so what is the point ?
sabercoy
sabercoyโ€ข2y ago
you should wrap it around any components that could throw errors, I dont think you need to worry about <Body /> since this is a solid-provided component I am not sure about the nature of your errors
Trader101
Trader101OPโ€ข2y ago
@davedbase hi to toggle between dark/light with solid-primitives, do I still add/remove dark class so it works with tailwindcss? @davedbase The whole issue was just tied to the fact I did not use Suspense!!
davedbase
davedbaseโ€ข2y ago
Oh around the router might make sense? You werenโ€™t calling any data though lol
Trader101
Trader101OPโ€ข2y ago
in the root.jsx yes also do you advise me to switch back to ssr: false ?
davedbase
davedbaseโ€ข2y ago
Again depends what youโ€™re trying to accomplish ๐Ÿ™‚
Trader101
Trader101OPโ€ข2y ago
well I mean something like etherscan ?
davedbase
davedbaseโ€ข2y ago
I donโ€™t known what that is, sorry
Trader101
Trader101OPโ€ข2y ago
Ethereum (ETH) Blockchain Explorer
Ethereum (ETH) Blockchain Explorer
Etherscan allows you to explore and search the Ethereum blockchain for transactions, addresses, tokens, prices and other activities taking place on Ethereum (ETH)
Trader101
Trader101OPโ€ข2y ago
anyways there is no client data stored since it is all done through web3 wallet so I don't need the server
davedbase
davedbaseโ€ข2y ago
Yeah sorry crypto and blockchain completely and totally elude me
Trader101
Trader101OPโ€ข2y ago
Although I would like to have some sort of logging system (which logs who connects, ip, etc and store it to text file) like morgan.js can I do this with csr
Want results from more Discord servers?
Add your server