How do I set a state to a value in localStorage? - javascript

There is a button that toggles dark and light mode, and the state of what mode the page is on is saved in localStorage. However, I cannot change the initial value of the state (dark) and I don't know why. This is done in a useEffect function but no matter what the value of dark is, it is always set to its initial value of false.
How do I set the value of the localStorage to the dark state?
function Mode() {
const [dark, setDark] = useState(false);
// localStorage.removeItem("dark");
const onClick = () => {
if (dark) {
setDark(false);
document.querySelector("body").classList.remove("dark");
} else {
setDark(true);
document.querySelector("body").classList.add("dark");
}
localStorage.setItem("dark", dark);
};
const localDark = JSON.parse(localStorage.getItem("dark"));
useEffect(() => {
if (localDark !== null) {
setDark(!JSON.parse(localStorage.getItem("dark"))); // this is what does not change the value of dark
onClick();
}
}, []);
return (
<div onClick={onClick} className="mode">
{dark ? <Light /> : <Dark />}
</div>
);
}

Directly use the value from localStorage in useState as the default. useEffect is unnecessary here.
const [dark, setDark] = useState(JSON.parse(localStorage.getItem("dark")));
document.body.classList.toggle('dark', dark);
The click event handler should set the localStorage dark value to the logical complement of the current value.
const onClick = () => {
localStorage.setItem("dark", !dark);
setDark(!dark);
};

Use a function to initialize the state from local storage. Update the storage and the body's class on init, and when dark state changes:
const getLocalDark = () => !!JSON.parse(localStorage.getItem("dark"));
function Mode() {
const [dark, setDark] = useState(getLocalDark);
const onClick = () => {
setDark(d => !d);
};
useEffect(() => {
const classList = document.querySelector("body").classList;
if (dark) classList.add("dark");
else classList.remove("dark");
localStorage.setItem("dark", dark);
}, [dark]);
return (
<div onClick={onClick} className="mode">
{dark ? <Light /> : <Dark />}
</div>
);
}

Perhaps you'd be interested in a useLocalStorage hook. Here's how that can be implemented:
export const useLocalStorage = (key, initialState) => {
const [value, setValue] = useState(() => {
// Initialize with the value in localStorage if it
// already exists there.
const data = localStorage.getItem(key);
// Otherwise, initialize using the provided initial state.
return data ? JSON.parse(data) : initialState;
});
// Each time "value" is changed using "setValue", update the
// value in localStorage to reflect these changes.
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [value]);
return [value, setValue];
};
This hook syncs the value seen in localStorage with the value stored in memory under the value variable.
The usage looks like this (almost identical to regular useState):
export const Counter = () => {
const [count, setCount] = useLocalStorage('count', 0);
return (
<div>
<p>{count}</p>
<button
onClick={() => {
setCount((prev) => prev + 1);
}}>
Increase count
</button>
</div>
);
};
However, the main caveat of this hook is that it's only really meant to be used in one component. That means, if you change the value from light to dark in one component using this hook, any other components using it won't be updated. So instead, you should look into using a similar implementation of what is demonstrated above, but using the React Context API. That way, you'll ensure your in-memory values are in sync with those stored in localStorage. Theming is one of the main uses of the Context API.
Good luck! :)

Related

force remount on value change

I have some files that builds a cart in a dropdown for my shop website.
One file adds the selected item to an array which will be my cart. The other file is the CartDropdown component itself. My cart only show the items when I close and open it (remounting), but I want it to remount every time I add a new item.
Adding item function:
const ProductContainer = ({ productInfo }) => {
const { cartProducts, setCartProducts } = useContext(CartContext);
const cartArray = cartProducts;
const addProduct = () => {
productInfo.quantity = 1;
if (cartArray.includes(productInfo)) {
const index = cartArray.findIndex((object) => {
return object === productInfo;
});
cartProducts[index].quantity++;
setCartProducts(cartArray);
} else {
cartArray.push(productInfo);
setCartProducts(cartArray);
}
// setCartProducts(cartArray)
console.log(cartProducts);
// console.log(cartArray)
};
};
dropdown component
const CartDropdown = () => {
const { setCartProducts, cartProducts } = useContext(CartContext);
const { setProducts, currentProducts } = useContext(ProductsContext);
// useEffect(() => {}, [cartProducts])
const cleanCart = () => {
const cleanProducts = currentProducts;
console.log(cleanProducts);
for (let i in cleanProducts) {
if (cleanProducts[i].hasOwnProperty("quantity")) {
cleanProducts[i].quantity = 0;
}
}
setProducts(cleanProducts);
setCartProducts([]);
};
return (
<div className="cart-dropdown-container">
<div className="cart-items">
{cartProducts.map((product) => (
<div key={product.id}>
<img src={product.imageUrl}></img>
</div>
))}
</div>
<button onClick={cleanCart}>CLEAN CART</button>
<Button children={"FINALIZE PURCHASE"} />
</div>
);
};
How can I force the dropdown to remount every time cartProducts changes?
CART CONTEXT:
export const CartContext = createContext({
isCartOpen: false,
setIsCartOpen: () => { },
cartProducts: [],
setCartProducts: () => { }
})
export const CartProvider = ({ children }) => {
const [isCartOpen, setIsCartOpen] = useState(false)
const [cartProducts, setCartProducts] = useState([])
const value = { isCartOpen, setIsCartOpen, cartProducts, setCartProducts };
return (
<CartContext.Provider value={value}>{children}</CartContext.Provider>
)
}
product context
export const ProductsContext = createContext({
currentProducts: null,
setProducts: () => {}
})
export const ProductsProvider = ({children}) => {
const [currentProducts, setProducts] = useState(shop_data)
const value = {currentProducts, setProducts}
return(
<ProductsContext.Provider value={value}>{children}</ProductsContext.Provider>
)
}
You can change the key prop of the component every time you want to remount. Every time cartProduct changes, update the value of key. You can do that using a useEffect with cartProduct as a dependency.
<CartDropdown key={1} />
to
<CartDropdown key={2} />
Edit for more clarification:
const [keyCount, setKeyCount] = useState(0);
useEffect(() => {
setKeyCount(keyCount+1);
}, [cartProducts]);
<CartDropdown {...otherProps} key={keyCount} />
The first issue I see is that you are not using the callback to set the state inside the context but you are doing cartProducts[index].quantity++ and react docs specify
Do Not Modify State Directly
Also after cartProducts[index].quantity++, you call setCartProducts(cartArray); not with cartProducts which you actually updated (this is also the reason why "if I do usestate(console.log('A'), [cartProducts]) its not triggering everytime i add my cart product". But anyway there is an issue even if you would use cartArray for both:
You shouldn't directly do const cartArray = cartProducts since by doing so cartArray will be a reference to cartProducts (not a copy of it) which also shouldn't be modified (because it would mean that you are modifying state directly).
So first 2 things I recommend you to improve would be:
Initialize cartArray as a cartProducts deep copy (if your cartProducts is an array of objects, spread syntax won't do it). So I would reccomand you to check this question answers for creating a deep copy.
After you make sure that cartArray is a deep copy of cartProducts, doublecheck you use cartArray to create a local newValue then set the state of the context with the same value (so basically:
cartArray[index].quantity++;
setCartProducts(cartArray);
)
The deep copy part also apply for const cleanProducts = currentProducts; (you should also create a deep copy here for cleanProducts, instead of saving the object ref).
If you are not using deep copies, your code might still work in some cases, but you might encounter weird behaviors in some other instances (and thoose are really hard to debug). Therefore is a bad practice in general not using deep copies.

React: usePrevious hook only works if there is 1 state in the component. How to make it work if there are multiple states?

The docs suggests the folllowing to get previous state:
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return <h1>Now: {count}, before: {prevCount}</h1>;
}
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
Per my understanding, this works fine only if there is exactly one state in the component. However consider the following where there are multiple states:
import "./styles.css";
import React, { useState, useEffect, useRef, useContext } from "react";
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
export default function App() {
const [count, setCount] = useState(0);
const [foo, setFoo] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<button onClick={() => setFoo(f => f+1)}> Update foo </button>
<h1>Now: {count}, before: {prevCount}</h1>
</div>);
}
Sandbox: https://codesandbox.io/s/little-feather-wow4m
When a different state (foo) is updated, the usePrevious hook returns the latest value for count, as opposed to the previous one).
Is there a way to reliably get the previous value for a state/prop when there are multiple states?
I don't think this is the right approach.
How about a custom hook that sets up the state and returns a custom setter function that handles this logic for you.
function useStateWithPrevious(initial) {
const [value, setValue] = useState(initial)
const [prev, setPrev] = useState(initial)
function setValueAndPrev(newValue) {
if (newValue === value) return // optional, depends on the logic you want.
setPrev(value)
setValue(newValue)
}
return [prev, value, setValueAndPrev]
}
Which you would use like:
function MyComponent() {
const [prevCount, count, setCount] = useStateWithPrevious(0)
}
I was able to create an altered version of your hook that does seem to work:
function usePrevious(value) {
const ref = useRef([undefined, undefined]);
if (ref.current[0] !== value) {
ref.current = [value, ref.current[0]];
}
return ref.current[1];
}
Playground here.
Keeping track of only the previous value of some state is not a very common use case. Hence there's no need to overthink it or try to "trick" React with refs to achieve a slightly shorter syntax. There's almost always a few ways to get the same result in a more straightforward and maintainable manner.
React's docs also stress that this suggested approach is only for edge cases.
This is rarely needed and is usually a sign you have some duplicate or redundant state.
If the previous count is de facto part of the application state (it's used in rendering just like the current count), it's counter productive to not just store it as such. Once it's in state, it's just a matter of making state updates in event listeners update all parts of the state in one go, making it inherently safe with React's concurrent features.
Method 1: Set multiple state variables at the same time
Just create an additional state variable for the old value, and make your handler set both values.
const initialCount = 0;
function App() {
const [count, setCount] = useState(initialCount);
const [prevCount, setPrevCount] = useState(initialCount);
return <>
<button
onClick={() => {
// You can memoize this callback if your app needs it.
setCount(count + 1);
setPrevCount(count);
}}
>Increment</button>
<span>Current: {count} </span>
<span>Previous: {prevCount} </span>
</>
}
You can almost always do this instead, it offers the same functionality as usePrevious and obviously will never lead to the application using the wrong combination of values. In fact, because of batched state updates since React 18 there's no performance penalty in calling 2 setters in one event handler.
Using a hook like usePrevious doesn't really bring any overall benefits. Clearly both the current and the previous value are pieces of state your application needs for rendering. They can both use the same simple and readable syntax. Just because usePrevious is shorter doesn't mean it's easier to maintain.
Method 2: useReducer
If you want to avoid the 2 function calls in the event listener, you can use useReducer to encapsulate your state. This hook is particularly well suited for updates of complex but closely related state. It guarantees the application state transitions to a new valid state in one go.
const initialState = { count: 0, prevCount: 0, foo: 'bar' };
function countReducer(state, action) {
switch (action.type) {
case: 'INCREMENT':
return {
...state,
count: state.count + 1,
prevCount: state.count,
};
case 'DO_SOMETHING_ELSE':
// This has no effect on the prevCount state.
return {
...state,
foo: payload.foo,
}
}
return state;
}
function App() {
const [
{ count, prevCount },
dispatch
] = useReducer(countReducer, initialState)
return <>
<button
onClick={() => {
dispatch({ type: 'INCREMENT' });
}}
>Increment</button>
<button
onClick={() => {
dispatch({
type: 'DO_SOMETHING_ELSE',
payload: { foo: `last update: ${prevCount} to ${count`} },
);
}}
>Do foo</button>
<span>Current: {count} </span>
<span>Previous: {prevCount} </span>
</>
}

Difference between usePreviousDistinct with useEffect and without

I needed a hook to get the previous distinct value of a specific state. It looks like this and it seems to work:
function usePreviousDistinct(state) {
const prevRef = useRef();
useEffect(() => {
prevRef.current = state;
}, [state]);
return prevRef.current;
}
I've also seen there is a usePreviousDistinct hook in the react-use package but the approach is different than mine.
import { useRef } from 'react';
import { useFirstMountState } from './useFirstMountState';
export type Predicate<T> = (prev: T | undefined, next: T) => boolean;
const strictEquals = <T>(prev: T | undefined, next: T) => prev === next;
export default function usePreviousDistinct<T>(value: T, compare: Predicate<T> = strictEquals): T | undefined {
const prevRef = useRef<T>();
const curRef = useRef<T>(value);
const isFirstMount = useFirstMountState();
if (!isFirstMount && !compare(curRef.current, value)) {
prevRef.current = curRef.current;
curRef.current = value;
}
return prevRef.current;
}
I wonder if I have not understood something or am missing something. Is my version also correct?
In my test I could not find a difference:
https://codesandbox.io/s/distracted-mayer-zpym8?file=/src/App.js
useEffect() together with useRef() (your version) does not show the latest value.
useRef() without useEffect() gives you the correct value, because it runs synchronously, but doesn't have the advantages that come with asynchronicity.
useEffect() together with useState() gives you the correct value, but might trigger unnecessary renders (adds potentially unnecessary overhead).
Your version looks like it works as expected, because the old value that is shown is the one that you expect to see as the new value. But the actual new value is not the one you want.
Example:
import React, { useState, useEffect, useRef } from 'react';
export const MyComponent = function(props){
const [state, setState ] = useState(0);
return <React.Fragment>
state: { state },<br />
with useEffect: { usePreviousDistinctUE( state ) },<br />
w/o useEffect: { usePreviousDistinctR( state ) },<br />
<button onClick={ function(){
setState( state + 1 );
} }>
increment
</button>
</React.Fragment>;
};
const usePreviousDistinctUE = function( value ){
const prevRef = useRef();
useEffect(() => {
prevRef.current = value;
console.log('with useEffect, prev:', prevRef.current, ', current:', value);
}, [value]);
return prevRef.current;
};
const usePreviousDistinctR = function( value ){
const prevRef = useRef();
const curRef = useRef( value );
if( curRef.current !== value ){
prevRef.current = curRef.current;
curRef.current = value;
}
console.log('w/o useEffect, prev:', prevRef.current, ', current:', curRef.current);
return prevRef.current;
};
The values shown on the page are the same, but in the console they are different. That means the value in the useEffect() version is changed, it is only not yet shown on the page.
If you just add another hook that updates anything unrelated (leaving everything else unchanged), then the page (might*) magically show the updated value again, because the page is re-rendered and the previously already changed value is shown. The value is now wrong in your eyes, but it is not changed, only shown:
// ...
with useEffect: { usePreviousDistinctUE( state ) },<br />
w/o useEffect: { usePreviousDistinctR( state ) },<br />
anything updated: { useAnythingUpdating( state ) },<br />
// ...
const useAnythingUpdating = function(state){
const [result, setResult ] = useState(0);
useEffect(() => {
setResult( state );
console.log('anything updated');
});
return result;
};
*But you shouldn't rely on something else triggering a re-render. I'm not even sure this would update as expected under all circumstances.
more Details:
useEffect() is triggered at some time when react decides that the prop must have been changed. The ref is changed then, but react will not 'get informed' about a ref change, so it doesn't find it necessary to re-render the page to show the changed value.
In the example without useEffect() the change happens synchronously.
React doesn't 'know' about this change either, but (if everything else runs as expected) there will be always a re-render when necessary anyway (you will have called that function from another function that is rendered at the end).
(Not informing react about the change is basically the point in using useRef(): sometimes you want just a value, under your own control, without react doing magic things with it.)

React multiple callbacks not updating the local state

I have a child component called First which is implemented below:
function First(props) {
const handleButtonClick = () => {
props.positiveCallback({key: 'positive', value: 'pos'})
props.negativeCallback({key: 'negative', value: '-100'})
}
return (
<div><button onClick={() => handleButtonClick()}>FIRST</button></div>
)
}
And I have App.js component.
function App() {
const [counter, setCounter] = useState({positive: '+', negative: '-'})
const handleCounterCallback = (obj) => {
console.log(obj)
let newCounter = {...counter}
newCounter[obj.key] = obj.value
setCounter(newCounter)
}
const handleDisplayClick = () => {
console.log(counter)
}
return (
<div className="App">
<First positiveCallback = {handleCounterCallback} negativeCallback = {handleCounterCallback} />
<Second negativeCallback = {handleCounterCallback} />
<button onClick={() => handleDisplayClick()}>Display</button>
</div>
);
}
When handleButtonClick is clicked in First component it triggers multiple callbacks but only the last callback updates the state.
In the example:
props.positiveCallback({key: 'positive', value: 'pos'}) // not updated
props.negativeCallback({key: 'negative', value: '-100'}) // updated
Any ideas?
Both are updating the state, your problem is the last one is overwriting the first when you spread the previous state (which isn't updated by the time your accessing it, so you are spreading the initial state). An easy workaround is to split counter into smaller pieces and update them individually
const [positive, setPositive] = useState('+')
const [negative, setNegative] = useState('-')
//This prevents your current code of breaking when accessing counter[key]
const counter = { positive, negative }
const handleCounterCallback = ({ key, value }) => {
key === 'positive' ? setPositive(value) : setNegative(value)
}
You can do that but useState setter is async like this.setState. If you want to base on the previous value you should use setter as function and you can store it in one state - change handleCounterCallback to
const handleCounterCallback = ({key,value}) => {
setCounter(prev=>({...prev, [key]: value}))
}
and that is all. Always if you want to base on the previous state use setter for the state as function.
I recommend you to use another hook rather than useState which is useReducer - I think it will be better for you

How do I fix HandleToggleState?

I have a simple material-ui toggle in my react component. I want to use it to toggle state (false/true). If I start with useState(false) the first time I click the toggle it says false rather than true.
I'm wondering if another react hook would solve for this. useEffect, useCallback...
const Component = () => {
const [toggleValue, setToggleValue] = useState(false);
const handleToggleChange = () => {
setToggleValue(!toggleValue);
console.log("toggle value in handle: " + toggleValue);
};
return(
<FormControlLabel
control={
<Switch
checked={toggleValue}
onChange={handleToggleChange}
value="my toggle"
/>
}
/>
);
};
I would expect setPreseason(!preseason); to set the state opposite of what it currently is. True to false and false to true.
It probably is but when I log the state on the next line it logs the initial state and will always log the opposite of what the toggle is.
The state updater function returned by useState is asynchronous
If you need to react to state changes useEffect is the place for it
const Component = () => {
const [toggleValue, setToggleValue] = useState(false);
const handleToggleValue = () => {
setToggleValue(!toggleValue);
};
useEffect(() => {
console.log("toggleValue: " + toggleValue);
// second argument to useEffect is an array of dependencies
// this function is going to run every time one of the dependencies
// changes
}, [toggleValue])
return (
<FormControlLabel
control={
<Switch
checked={toggleValue}
onChange={handleToggleValue}
value="my toggle"
/>
}
/>
);
}
The issue is about which value toggleValue is inside the closure. Is not what you expect. Instead pass a callback to setToggleValue. You will get the current state back, that you can then change.
const handleToggleValue = () => {
setToggleValue((toggleValue) => !toggleValue);
}
You are doing it correctly, except that toggleValue is just a local variable and is not changed just by calling setToggleValue (the state will be changed, but that happens asynchronously).
You could do something like this instead:
const handleToggleValue = () => {
const newToggleValue = !toggleValue;
setToggleValue(newToggleValue);
console.log('toggleValue: ' + newToggleValue);
}
It depends what your intention is with the new toggle value. This simply fixes what you were doing with the console.log call. But you may run into further trouble after that, given that you are using the new toggle value before the state is updated. But that is for another SO question...

Categories

Resources