I'm creating a static web app using React as a frontend and am running into a problem with dynamic theming. While the theme switching and saving the name of the theme into local storage works fine, it will only actually work once until a page refresh or a route change away and back to the page.
Ex. I can load the page with my previously chosen dark theme, but then if I were to switch back to a light theme, things work correctly, but then another switch to the dark theme causes the ui to change colors as intended but the local storage variable would stay as the light theme.
Code below
These are the html cards that showcase and change a theme
const ThemeCards = ( props ) => {
// eslint-disable-next-line
const [theme, setTheme] = useLocalStorage("theme-type", "theme-default");
var themeToggle = (themeName) => {
setTheme(themeName);
document.getElementById("themeProvider").className = themeName;
}
return (
<div className={props.className}>
<div className={props.className "-inner"}>
<div className={props.className "-front"}>
<div>{props.themeName}</div>
</div>
<div className={props.className "-back"}>
<button onClick={() => themeToggle(props.themeID)}>Toggle</button>
</div>
</div>
</div>
)
}
This is the useLocalStorage.js
import { useEffect, useState } from 'react';
const useLocalStorage = (storageKey, fallbackState) => {
const [value, setValue] = useState(
JSON.parse(localStorage.getItem(storageKey)) ?? fallbackState
);
useEffect(() => {
localStorage.setItem(storageKey, JSON.stringify(value));
}, [value, storageKey]);
return [value, setValue];
}
export default useLocalStorage;
Implementation of themeProvider, it is simply the id of a div providing the theme name as a string as the className
function App() {
const [theme, setTheme] = useLocalStorage("theme-type",
"theme-default");
return (
<>
<div className={theme} id="themeProvider">
<HashRouter>
<SideBar />
<NavTitle />
<Routes>
...
</Routes>
</HashRouter>
</div>
</>
);
}
export default App;
And versions
"react": "^18.1.0",
"react-dom": "^18.1.0"
"react-router-dom": "^6.3.0",
"react-scripts": "5.0.1",
Refreshing the page or simply navigating to the home route and then back to the themes route will reset the cycle.
I've console logged both the theme and themeName hoping to debug using this if statement in the themeToggle
if (theme === themeName) {
console.log("Theme is already set to " themeName);
return
} else {
setTheme(themeName);
document.getElementById("themeProvider").className = themeName;
}
Although when the error happens, only one variable exists in the local storage yet switching to any theme yields the console log branch in the console, as if saying the local storage key represents both strings at the same time, any guidance would be appreciated as I'm rather new to persisting data in the browser!
CodePudding user response:
In React it is not recommended to directly use getElementById and modify the DOM. It is very likely to confuse people if that is used because now the DOM is both being manually controlled and being controlled by React.
There are more suspicious things with ThemeToggle(props.themeID)
. The capitalization does not match var themeToggle
so it might not even be running. In this example it is not necessary so it should be replace with just setValue
. The props.themeID
also stands out. Is the proper string being passed in?
When you said "as if saying the local storage key represents both strings at the same time", have you tried logging in both branches? React is allowed to rerender the same data before it renders the new data, so it may have just logged the previous value.
I pasted the given code into a new create-react-app and got it to work as intended:
App.jsx:
import React, {useEffect, useState} from 'react';
import "./App.css"
const useLocalStorage = (storageKey, fallbackState) => {
const [value, setValue] = useState(
JSON.parse(localStorage.getItem(storageKey)) ?? fallbackState
);
useEffect(() => {
localStorage.setItem(storageKey, JSON.stringify(value));
}, [value, storageKey]);
return [value, setValue];
}
const App = () => {
const [theme, setTheme] = useLocalStorage("theme-type", "theme-default");
const otherTheme = theme === "theme-default" ? "theme-dark" : "theme-default";
return (
<div className={"" theme}>
<div className={theme "-inner"}>
<div className={theme "-front"}>
<div>{theme}</div>
</div>
<div className={theme "-back"}>
<button onClick={() => setTheme(otherTheme)}>Toggle</button>
</div>
</div>
</div>
)
}
export default App;
App.css:
.theme-default {
padding: 10px;
background: #f00;
}
.theme-default-inner {
padding: 10px;
background: #0f0;
}
.theme-default-front {
padding: 10px;
background: #ff0;
}
.theme-default-back {
padding: 10px;
background: #00f;
}
.theme-dark {
padding: 10px;
background: #a00;
}
.theme-dark-inner {
padding: 10px;
background: #0a0;
}
.theme-dark-front {
padding: 10px;
background: #aa0;
}
.theme-dark-back {
padding: 10px;
background: #00a;
}
If it is necessary to pass the data to the rest of the app then it is possible to extend this example using Context. A pair of context providers can be used to wrap the whole app, one providing the theme
and one providing the setTheme
. They can be retrieved with useContext
. The more advanced use case would be satisfied by the following:
import React, {createContext, useContext, useEffect, useState} from 'react';
import "./App.css"
const ThemeContext = createContext("theme-default");
const SetThemeContext = createContext((_) => {});
const useLocalStorage = (storageKey, fallbackState) => {
const [value, setValue] = useState(
JSON.parse(localStorage.getItem(storageKey)) ?? fallbackState
);
useEffect(() => {
localStorage.setItem(storageKey, JSON.stringify(value));
}, [value, storageKey]);
return [value, setValue];
}
const ThemeCards = (props) => {
const setTheme = useContext(SetThemeContext);
return (
<div className={props.className}>
<div className={props.className "-inner"}>
<div className={props.className "-front"}>
<div>{props.className}</div>
</div>
<div className={props.className "-back"}>
<button onClick={() => setTheme(props.className)}>Toggle</button>
</div>
</div>
</div>
)
}
const OtherComponent = () => {
const theme = useContext(ThemeContext);
return (
<article className={"" theme}>Current theme is {theme}</article>
)
}
const App = () => {
const [theme, setTheme] = useLocalStorage("theme-type", "theme-default");
return (
<SetThemeContext.Provider value={setTheme}>
<ThemeContext.Provider value={theme}>
<OtherComponent/>
<br/>
<ThemeCards className={"theme-default"}></ThemeCards>
<ThemeCards className={"theme-dark"}></ThemeCards>
</ThemeContext.Provider>
</SetThemeContext.Provider>
)
}
export default App;
CodePudding user response:
Try passing the setting method to the child, it looks like the parent is not updating when then child updates the theme.
const [theme, setTheme] = useLocalStorage("theme-type",
"theme-default");
return (
<>
<div className={theme} id="themeProvider">
<HashRouter>
<SideBar />
<NavTitle />
<Routes>
...
<Route path={`/${appRoutes.home}`} element={<Home setTheme={setTheme} />} />
</Routes>
</HashRouter>
</div>
</>
);
}
or use a context