Repository navigation
How do I ensure my dark mode persists with local storage? #1158
|
Hi there, I am attempting to enable dark mode with the help of local storage. I am able to actually toggle between the light and dark mode and I even see the root class change on the toggle. However, my issue is when I refresh the page, the theme is reset. const ThemeToggler = () => {
const [theme, setTheme] = createSignal<string>('dark');
createEffect(() => {
let theme: 'light' | 'dark';
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
theme = localStorage.getItem('theme') as 'light' | 'dark';
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
theme = 'dark';
} else {
theme = 'light';
}
if (theme === 'light') {
setTheme('light');
} else {
setTheme('dark');
}
});
const toggleTheme = () => {
const root = document.documentElement;
if (theme() === 'dark') {
root.classList.remove('theme-dark');
localStorage.setItem('theme', 'light');
setTheme('light');
} else {
root.classList.add('theme-dark');
localStorage.setItem('theme', 'dark');
setTheme('dark');
}
};
return <button onClick={() => toggleTheme()}>Click Me</button>;
};
export default ThemeToggler;I recreated this set up in the Solid playground: https://playground.solidjs.com/?hash=282373262&version=1.4.1 |
Replies: 5 comments 8 replies
|
Yeah it took me a min to follow but the issue here is that the logic that actually produces the effect is in the toggle. So it only changes when you press the button. That logic needs to be in an effect so that it runs when the page first loads. Whereas your current effect doesn't actually track values so it is only for initialization so it doesn't need to go in an effect at all. So I changed it so that all things are derived from the signal and the event just toggles it: const initializeTheme = () => {
let theme;
if (typeof localStorage !== "undefined" && localStorage.getItem("theme")) {
theme = localStorage.getItem("theme") as "light" | "dark";
} else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
theme = "dark";
} else {
theme = "light";
}
return theme;
};
const ThemeToggler = () => {
const [theme, setTheme] = createSignal<string>(initializeTheme());
createEffect(() => {
const root = document.documentElement;
if (theme() === "light") {
root.classList.remove("theme-dark");
localStorage.setItem("theme", "light");
} else {
root.classList.add("theme-dark");
localStorage.setItem("theme", "dark");
}
});
return (
<div>
<button onClick={() => setTheme((t) => (t === "light" ? "dark" : "light"))}>{theme()}</button>
</div>
);
};
render(() => <ThemeToggler />, document.getElementById("app")!);I made a working example here: https://playground.solidjs.com/?hash=2047679767&version=1.4.1 |
|
Hi @ryansolid, I have a follow up questions concerning this example. When running your ThemeToggle in a fresh solid-start (bar + SSR + Typescript) environment:
Is there anything I need to account for in an SSR environment? Thanks a lot for your help |
|
Hello! Sorry for posting in an old thread. 🙈 I've come across the same issue. There are a few problems actually:
Here is a demo of the problem (note that I reload the page a few times): 1.mp4I tried to approach the problem with cookies and with some other workarounds, but I failed. Eventually, I hacked a solution (far from perfect, but hopefully someone could point me in the right direction): import './app.css';
import { MetaProvider } from '@solidjs/meta';
import { Router } from '@solidjs/router';
import { FileRoutes } from '@solidjs/start/router';
import { createSignal, onMount, Suspense } from 'solid-js';
import { twMerge } from 'tailwind-merge';
import { Switch } from './components/Switch';
import MoonIcon from './icons/moon.svg?component-solid';
import SunIcon from './icons/sun.svg?component-solid';
const persistIsLightModeEnabled = (isLightModeEnabled: boolean) => {
window.localStorage.setItem('isLightModeEnabled', JSON.stringify(isLightModeEnabled));
};
const [isLightModeEnabled, setIsLightModeEnabled] = createSignal(false);
const updateDocumentDataTheme = (newIsLightModeEnabled: boolean) =>
document.documentElement.setAttribute('data-theme', newIsLightModeEnabled ? 'light' : 'dark');
const addThemeAttributeScript = `
(function() {
window.getIsLightModeEnabledFromPersistence = () => {
if (window.localStorage.getItem('isLightModeEnabled') === null) {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
return window.localStorage.getItem('isLightModeEnabled') === 'true';
};
document.documentElement.setAttribute('data-theme', window.getIsLightModeEnabledFromPersistence() ? 'light' : 'dark');
})();
`;
export default function App() {
return (
<Router
root={(props) => (
<MetaProvider>
{/* eslint-disable-next-line solid/no-innerhtml */}
<script innerHTML={addThemeAttributeScript} />
<Header />
<main>
<Suspense>{props.children}</Suspense>
</main>
</MetaProvider>
)}
>
<FileRoutes />
</Router>
);
}
/**
* This script is used to toggle the theme switch when the page is loaded
* and before the first render of the theme switch to workaround annoying flash of its UI state.
* TODO: Is there no better way to do this?
*/
const toggleThemeSwitchScript = `
(function() {
const updateThemeSwitch = (addedNode) => {
if (!(addedNode instanceof HTMLElement) || addedNode.id !== "theme-switch") {
return false;
}
const isLightModeEnabled = window.getIsLightModeEnabledFromPersistence();
addedNode.querySelector("input").checked = isLightModeEnabled;
if (isLightModeEnabled) {
addedNode.querySelector("#theme-switch-sun-icon").classList.remove("hidden");
} else {
addedNode.querySelector("#theme-switch-moon-icon").classList.remove("hidden");
}
return true;
};
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (addedNode of mutation.addedNodes) {
if (!updateThemeSwitch(addedNode)) {
continue;
}
observer.disconnect();
return;
}
}
console.error("Failed to find theme switch");
observer.disconnect();
})
observer.observe(document.getElementById('theme-switch-container'), { childList: true, subtree: true });
})();
`;
function Header() {
const [isMounted, setIsMounted] = createSignal(false);
onMount(() => {
setIsMounted(true);
});
onMount(() => {
// Sync the theme switch state with the persisted state
setIsLightModeEnabled(
(
window as typeof window & { getIsLightModeEnabledFromPersistence: () => boolean }
).getIsLightModeEnabledFromPersistence(),
);
});
return (
<>
<header>
<div id="theme-switch-container">
{/* eslint-disable-next-line solid/no-innerhtml */}
<script innerHTML={toggleThemeSwitchScript} />
<Switch
id="theme-switch"
thumbIcon={
<>
<SunIcon
id="theme-switch-sun-icon"
class={twMerge(
// Have Solid.js to alter the hidden class only after mounting and let the inline script
// to handle the initial state to avoid flickering.
(!isMounted() || !isLightModeEnabled()) && 'hidden',
'p-0.5 w-full bg-slate-100 rounded-full text-slate-600 transition-colors',
)}
/>
<MoonIcon
id="theme-switch-moon-icon"
class={twMerge(
(!isMounted() || isLightModeEnabled()) && 'hidden',
'p-0.5 w-full bg-slate-950 rounded-full text-slate-100 transition-colors',
)}
/>
</>
}
// aka onToggle
onChange={(event) => {
const target = event.target as HTMLInputElement;
const isLightModeEnabled = target.checked;
setIsLightModeEnabled(isLightModeEnabled);
updateDocumentDataTheme(isLightModeEnabled);
persistIsLightModeEnabled(isLightModeEnabled);
}}
/>
</div>
</header>
</>
);
}I use two inline scripts:
Result: 3.mp4 |
|
We also have makePersisted in our |
|
@atk, thank you! I tried to use this package among the first attempts to solve the problem. Once I copy and paste the code from the NPM page right in the import { MetaProvider, Title } from "@solidjs/meta";
import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { createSignal, Suspense } from "solid-js";
import "./app.css";
import { cookieStorage, makePersisted } from '@solid-primitives/storage';
const [state, setState] = makePersisted(createSignal(), {
storage: cookieStorage.withOptions({ expires: new Date(+new Date() + 3e10) }),
});
export default function App() {
return (
<Router
root={props => (
<MetaProvider>
<Title>SolidStart - Basic</Title>
<a href="/">Index</a>
<a href="/about">About</a>
<Suspense>{props.children}</Suspense>
</MetaProvider>
)}
>
<FileRoutes />
</Router>
);
}The project fails: It can be reproduced in a brand new Solid.js project created with |
Yeah it took me a min to follow but the issue here is that the logic that actually produces the effect is in the toggle. So it only changes when you press the button. That logic needs to be in an effect so that it runs when the page first loads. Whereas your current effect doesn't actually track values so it is only for initialization so it doesn't need to go in an effect at all.
So I changed it so that all things are derived from the signal and the event just toggles it: