Local storage in react isn't keeping state on a refresh - javascript

I'm trying to keep state on a refresh in my React application using this code:
const [darkMode, setDarkMode] = useState(localStorage.getItem('darkMode') || false);
const [mode, setMode] = useState({bg: 'light', variant: 'light'})
const toggleDarkMode = () => {
if (darkMode === true) {
setMode({bg: 'light', variant: 'light'})
setDarkMode(false);
localStorage.setItem("darkMode", false );
} else {
setMode({bg: 'dark', variant: 'dark'})
setDarkMode(true);
localStorage.setItem("darkMode", true );
}
};
toggleDarkMode is called by a button onChange event.
But when I refresh or go to a different URL the state is lost. It's important to keep this state as its for the dark / light mode.
I have tried calling local storage using window.localStorage and tried to place it in useEffect so it would update constantly but the state still seems to be lost. I've also tried parsing it as JSON which a lot of tutorials recommend but that also doesn't seem to work.
Occasionly on a new page instance I'll get the error about bg cannot be undefined, which makes me think the state isn't being stored.
Why is the state being lost and is there a better / more efficient way to do this?

Local Storage can only store Strings. By updating toggleDarkMode to check against string values x or y I can solve the problem.
const [bool, setBool] = useState(localStorage.getItem('bool') || 'False')
const [mode, setMode] = useState((bool === 'False') ? {bg: 'light', variant: 'light'} : {bg: 'dark', variant: 'dark'})
const toggleDarkMode = () => {
if (bool === 'True') {
setMode({bg: 'light', variant: 'light'})
setBool('False')
localStorage.setItem("bool", 'False' );
} else {
setMode({bg: 'dark', variant: 'dark'})
setBool('True')
localStorage.setItem("bool", 'True' );
}
};
Credit to DBS and assembler

dark mode value upbate using contexApi or redux. Its have two benefits
not need local state setDarkMode
When the update dark mode it provides an updated value.
use this ?? instead of || optare.
``` const [darkMode, setDarkMode] = useState(localStorage.getItem('darkMode')?? false);

Related

State and localStorage data is different after refresh

So I have a problem with state and localStorage. To be exact, whenever I nest the objects inside of the themes.dark and themes.light the results in dynamic codeblock differ after refresh even though the localStorage data is in fact correct.
import { useState, useEffect } from "react";
import store from "store2";
import "./styles.css";
const themes = {
dark: {
theme: "dark"
},
light: {
theme: "light"
}
};
export default function App() {
const defaultDarkTheme = window.matchMedia("(prefers-color-scheme:dark)")
.matches
? themes.dark
: themes.light;
const [theme, setTheme] = useState(store.get("theme") || defaultDarkTheme);
useEffect(() => {
store.set("theme", theme);
}, [theme]);
return (
<>
<div
className="App"
onClick={() =>
theme === themes.dark ? setTheme(themes.light) : setTheme(themes.dark)
}
>
{JSON.stringify(theme)}
</div>
{theme === themes.dark
? JSON.stringify(themes.dark)
: JSON.stringify(themes.light)}
</>
);
}
Before refresh :
After refresh :
It's pretty problematic since after refresh instead of generating the content for dark mode it generates the content for the light one.
I think it is because of your useEffect hook.
useEffect(() => {
store.set("theme", theme);
}, [theme]);
I think your useEffect runs before it gets the data from the localStorage. I'm not sure this is just a hunch.
Your problem is not with state or the storage. You will see your problem if you add the following line after declaring your state and refresh the page after changing it to dark.
console.log(theme, themes.dark, theme === themes.dark)
Javascript only decides that two objects are equal if they refer to the exact same object. However when you get your theme object from the store, it is not the exact same object as the themes.dark object you've defined earlier.
There are multiple ways to handle a scenario like this, but I think it would be simplest if you compare some string properties of the objects, for example,
theme.theme === themes.dark.theme
? JSON.stringify(themes.dark)
: JSON.stringify(themes.light)
alternatively you could define a unique identifier property on your theme object which you can use to compare instead of theme.theme or if you really want to compare the objects you could do something like
JSON.stringify(theme) === JSON.stringify(themes.dark)
? JSON.stringify(themes.dark)
: JSON.stringify(themes.light)

Next.js making a timeout stop with a react state

Here is my component code (it is called on a page with <DetectPitch />);
import { useEffect, useState } from 'react';
export default function DetectPitch() {
const [detect, setDetect] = useState(false);
useEffect(() => {
document.getElementById("mute-button").addEventListener("click", () => setDetect(detect => !detect))
}, []);
useEffect(() => {
function update(random) {
if (detect != false) {
console.log("updating", random)
window.setTimeout(() => update(random), 100);
}
}
const audioContext = new window.AudioContext();
if (detect) {
audioContext.resume()
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
update(Math.random());
});
} else {
audioContext.suspend()
}
}, [detect]);
return (
<button id="mute-button">{detect ? "Mute" : "Unmute"}</button>
)
}
This component renders a button that when pressed toggles between mute/unmute based on the value of a react state, detect. It also sets up listening to a users audio input device (I believe the audioContext is being set multiple times but thats a seperate issue right now). I would like the browser to stop listening to the user audio input device when the button mute is pressed and stop logging "updating" to the console.
With the code as it currently is the audioContext never stops and the message continues to log, this means that multiple presses of the button creates new timeouts that are looped infinitely at an increasing rate (this is demonstrated by the random number printed to the console, depending on how many times you have clicked the button the console displays multiple different random numbers).
I think this is happening because javascript is passing by value rather than reference and therefore the state never changes internally to the random() function. I have tried using a getter function for detect but that doesn't change anything and i've considered creating an object to hold the state but that makes the code more complex. I feel like there is a simpler options that i'm missing.
For now I would like to be able to get the timeout to stop printing so that I can continue debugging the functionality to use a single instance of audioContext.
The issue seems to be that update function which is called periodically does not have access to the latest detect state from useState() hook.
Some changes in functionality compared to the original code:
AudioContext has it's own state - one of 'suspended', 'running', 'closed' or 'interrupted'. So mirroring has to be setup to update detect React state so React can re-render every time AudioContext state changes.
click handler was changed according to React's event handling
setTimeout was replaced with setInterval for convenience
cleanup added closing AudioContext when component is unmounted
loading state displayed till user grants access to a microphone
For update function to get latest detect value I'm calling setDetect with a callback. This looks hacky to me but it works, maybe a class component implementation is better (see bellow).
import { useEffect, useState } from 'react';
export default function DetectPitch() {
const [detect, setDetect] = useState(false);
// audioContext created after first render, initially set to null
const [audioContext, setAudioContext] = useState(null);
function update(random) {
// access current value of 'detect' by calling 'setDetect'
setDetect(detect => {
if (detect) {
console.log("updating", random)
}
return detect;
});
}
useEffect(() => {
const context = new window.AudioContext();
// Update 'detect' every time audiocontext changes state
// true - if running
// false - if not running (suspended, closed or interrupted (if in Safari))
context.addEventListener('statechange', (event) => {
console.log('audioContext changed state to: ' + event.target.state);
const isRunning = event.target.state === 'running'
setDetect(isRunning);
});
setAudioContext(context);
// start calling 'update'
const rand = Math.random();
window.setInterval(() => update(rand), 1000);
// cleanup when component is unmounted
return () => {
if (audioContext) {
// close if audioContext was created
audioContext.close();
}
}
}, []); // create audioContext only once on initial render
function onClickHandler() {
if (detect) {
audioContext.suspend();
} else {
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
audioContext.createMediaStreamSource(stream);
audioContext.resume();
});
}
}
// show Loading state while we create audioContext
if (!audioContext) {
return 'Loading...';
}
return (
<button onClick={onClickHandler}>
{detect ? "Mute" : "Unmute"}
</button>
)
}
Same implementation using class component:
import React from "react";
export default class DetectPitchClass extends React.Component {
constructor(props) {
// boilerplate
super(props);
this.update = this.update.bind(this);
this.onClickHandler = this.onClickHandler.bind(this);
// initial state
this.state = {
audioContext: null,
detect: false
};
}
componentDidMount() {
// initialised only once
const audioContext = new window.AudioContext();
// 'detect' mirrors state of audioContext
// true - if 'running'
// false - if not running (suspended, closed or interrupted)
// Safari changes state to interrupted if user switches to another tab
audioContext.addEventListener('statechange', (event) => {
console.log('audioContext changed state to: ' + event.target.state);
this.setState({ detect: event.target.state === 'running' });
});
this.setState({ audioContext });
// start calling 'update'
const rand = Math.random();
window.setInterval(() => this.update(rand), 1000);
}
componentWillUnmount() {
if (this.state.audioContext) {
// close if audioContext was created
this.state.audioContext.close();
}
}
// runs periodically, can always read 'detect' state
update(random) {
if (this.state.detect) {
console.log("updating", random)
}
}
onClickHandler() {
if (this.state.audioContext) {
if (this.state.detect) {
this.state.audioContext.suspend();
} else {
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
this.state.audioContext.createMediaStreamSource(stream);
this.state.audioContext.resume();
});
}
}
}
render() {
// show Loading state while we create audioContext
if (!this.state.audioContext) {
return 'Loading...';
}
return (
<button onClick={this.onClickHandler}>
{this.state.detect ? "Mute" : "Unmute"}
</button>
)
}
}
For completion sake, after doing more research. I have discovered that these sorts of problems are that Redux and advanced React Hooks (useContext and useReducer) set out to solve.

In GatsbyJS, when passing props to a linked page, how do I set a defaultProp?

I'm experiencing a TypeError: Cannot read property 'serviceCategory' of null issue when the "linked page" refreshes (f5) or when the page is visited as direct traffic.
I've tried setting defaultProps, but it's not triggering.
Here is my current setup:
<Link
to="/locations"
state={{
serviceCategory: "FILTER",
}}
>
linked page
const Locations = ({ location }) => {
const { state = {} } = location
const { serviceCategory } = state
const [category, setCategory] = useState(() => {
return location.state === null ? "ALL" : serviceCategory
})
}
...
Locations.defaultProps = {
location: {
state: {
serviceCategory: "ALL",
},
},
}
export default Locations
defaultProps is no where to be found in Gatsby's documentation, so i'm thinking of a different solution.
The defaultProps won't work in this case because gatsby uses #reach/router internally and props.location value is overriden by that.
You can either use a different variable name inside defaultProps or use a default value for state.
const state = location.state || { serviceCategory: 'ALL' }
Notice that
const { state = {serviceCategory:'ALL'} } = location
won't work because default initializer in destructuring only works for undefined values and not null values. And your value is null in this case.

How do I set system preference dark mode in a react app but also allow users to toggle back and forth the current theme

I have a react web app with a theme toggle on the navigation. I have a ThemeProvider Context that has logic to auto detects a user's System theme preference and sets it. However, I feel a user should be able to toggle themes back and forth on the website despite their system preference. Here is the ThemeContext.js file with all the theme logic including the toggle method.
import React, { useState, useLayoutEffect } from 'react';
const ThemeContext = React.createContext({
dark: false,
toggle: () => {},
});
export default ThemeContext;
export function ThemeProvider({ children }) {
// keeps state of the current theme
const [dark, setDark] = useState(false);
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)')
.matches;
const prefersLight = window.matchMedia('(prefers-color-scheme: light)')
.matches;
const prefersNotSet = window.matchMedia(
'(prefers-color-scheme: no-preference)'
).matches;
// paints the app before it renders elements
useLayoutEffect(() => {
// Media Hook to check what theme user prefers
if (prefersDark) {
setDark(true);
}
if (prefersLight) {
setDark(false);
}
if (prefersNotSet) {
setDark(true);
}
applyTheme();
// if state changes, repaints the app
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dark]);
// rewrites set of css variablels/colors
const applyTheme = () => {
let theme;
if (dark) {
theme = darkTheme;
}
if (!dark) {
theme = lightTheme;
}
const root = document.getElementsByTagName('html')[0];
root.style.cssText = theme.join(';');
};
const toggle = () => {
console.log('Toggle Method Called');
// A smooth transition on theme switch
const body = document.getElementsByTagName('body')[0];
body.style.cssText = 'transition: background .5s ease';
setDark(!dark);
};
return (
<ThemeContext.Provider
value={{
dark,
toggle,
}}>
{children}
</ThemeContext.Provider>
);
}
// styles
const lightTheme = [
'--bg-color: var(--color-white)',
'--text-color-primary: var(--color-black)',
'--text-color-secondary: var(--color-prussianBlue)',
'--text-color-tertiary:var(--color-azureRadiance)',
'--fill-switch: var(--color-prussianBlue)',
'--fill-primary:var(--color-prussianBlue)',
];
const darkTheme = [
'--bg-color: var(--color-mirage)',
'--text-color-primary: var(--color-white)',
'--text-color-secondary: var(--color-iron)',
'--text-color-tertiary: var(--color-white)',
'--fill-switch: var(--color-gold)',
'--fill-primary:var(--color-white)',
];
So when the page loads, show the user's system preferred them but also allow user to toggle themes by clicking a toggle button that fires the toggle function. In my current code, when toggle is called, it seems that state changes occur twice and therefore theme remains unchanged. How do I ensure the toggle method works correctly?
Here is the web app in question
Although Barry's solution is working, note that instead of adding more code, you could achieve the same result by skimming it:
The key is to set the user's preference as initial state and stop checking it in the effect:
export function ThemeProvider({ children }) {
/* Because you are setting the initial theme to non-dark,
you can assume that your initial state should be dark only
when the user's preference is set to dark. */
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)')
.matches;
// True if preference is set to dark, false otherwise.
const [dark, setDark] = useState(prefersDark);
/* Note: Initial state is set upon mounting, hence is better
to put the <ThemeProvider> up in your tree, close to the root <App>
to avoid unmounting it with the result of reverting to the default user
preference when and if re-mounting (unless you want that behaviour) */
useLayoutEffect(() => {
/* You end up here only when the user takes action
to change the theme, hence you can just apply the new theme. */
applyTheme();
}, [dark]);
...
CodeSandbox example
Why don't use simply useEffect?
useEffect(() => {
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
if (prefersDark) {
setIsDark(true);
}
}, []);
The reason to access window from useEffect: Window is not defined in Next.js React app.
The problem is that the whole block of useLayoutEffect runs every the dark value changes. So when the user toggles dark, the prefers... if statements run and setDark back to the system preference.
To solve this you'll need to keep track of the user manually toggling the theme and then preventing the prefers... if statements from running.
In your ThemeProvider do the following:
Add a state to monitor if the user has used toggle
const [userPicked, setUserPicked] = useState(false);
Update your toggle function:
const toggle = () => {
console.log('Toggle Method Called');
const body = document.getElementsByTagName('body')[0];
body.style.cssText = 'transition: background .5s ease';
setUserPick(true) // Add this line
setDark(!dark);
};
Finally, update the useLayout to look like this:
useLayoutEffect(() => {
if (!userPicked) { // This will stop the system preferences from taking place if the user manually toggles the them
if (prefersDark) {
setDark(true);
}
if (prefersLight) {
setDark(false);
}
if (prefersNotSet) {
setDark(true);
}
}
applyTheme();
}, [dark]);
Your toggle component shouldn't have to change.
Update:
Sal's answer is a great alternative. Mine points out the flaw in existing code and how to add to it. This points out how to which your code more effectively.
export function ThemeProvider({ children }) {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const [dark, setDark] = useState(prefersDark);
useLayoutEffect(() => {
applyTheme();
}, [dark]);
...
}
For everyone who wants to subscribe to changes of the system wide color scheme:
I extended #Daniel Danielecki's great answer:
useEffect(() => {
const mq = window.matchMedia(
"(prefers-color-scheme: dark)"
);
if (mq.matches) {
setIsDark(true);
}
// This callback will fire if the perferred color scheme changes without a reload
mq.addEventListener("change", (evt) => setIsDark(evt.matches === "dark"));
}, []);
By adding an event listener to the media query, you can listen to changes in the dark theme. This is useful if your user has an adaptive dark/light mode cycle based on their current time.

Get and use the previous state from local storage

I am building 3 diffent modes/radio buttons with ant design react and I will like to get the previous state from the local-storage and assign it to my value.
It works only when I hard code the value, there must be a cleaner way to achieve this.
class Settings extends React.Component {
state = {
mode: 'Something Mode 1',
}
componentDidMount() {
chrome.storage.local.get('AllModes', (result) => {
this.setState({ mode: result.AllModes})
})
}
handleMode () {
let currentMode = localStorage.getItem(['AllModes']);
console.log(currentMode)
this.setState(( currentMode ) => {
return {
mode: currentMode.mode
}
})
}
<RadioGroup onChange={this.onChange}
className="radio__group"
defaultValue={this.handleMode}>
</RadioGroup>
}
There are several ways to do this, but I'll describe a simple one.
You can persist the component's state in localStorage as a JSON-encoded string and restore it when the component is recreated.
We can define our save and loading methods:
function getStoredComponentState(initialState) {
const storedState = localStorage.getItem(checkedStorageKey);
return storedState ? JSON.parse(storedState) : initialState;
}
function storeComponentState(state) {
localStorage.setItem(checkedStorageKey, JSON.stringify(state))
}
We initialize our state from what was stored, providing a default in case there is no stored previous state:
constructor() {
super();
this.state = getStoredComponentState({
checked: false,
})
}
We persist any changes made to storage when they happen:
setState(state) {
super.setState(state);
storeComponentState(state);
}
And it works! You can make a change, refresh the page, and see that the previous state is restored.
JSFiddle

Categories