How to change state when localStorage value changed. For example, I have a language switching button, like French and English, when I click English, it will be storing to localStorage, when I click English it will also.
When I click the French the whole project need to see in French, also when I click English, want to do like that, it So how can I change state when I update localStorage?
<button onclick={()=>localStorage.setItem("language",'english')}>English</button>
<button onclick={()=>localStorage.setItem("language",'french')}>French</button>
let language;
if (typeof window !== "undefined") {
if (localStorage.getItem("language") === null) {
language = "english";
}
if (localStorage.getItem("language") !== null) {
language = localStorage.getItem("language");
}
}
const [langu, setLangua] = useState(language);
console.log(langu);
One way to achieve this that wouldn't change that much your current structure is first to change your buttons to this:
<button
onClick={() => {
localStorage.setItem("language", "english");
window.dispatchEvent(new Event("storage"));
}}
>
English
</button>
<button
onClick={() => {
localStorage.setItem("language", "french");
window.dispatchEvent(new Event("storage"));
}}
>
French
</button>
And then set up inside the component where you have setLangua and langu an useEffect that would listen to changes in the localStorage and update the state:
useEffect(() => {
const listenStorageChange = () => {
if (localStorage.getItem("language") === null) {
setLangua("english");
} else {
setLangua(localStorage.getItem("language"));
}
};
window.addEventListener("storage", listenStorageChange);
return () => window.removeEventListener("storage", listenStorageChange);
}, []);
you need set it in useEffect hook, with empty dependences, it will run only when the component mount.
const [langu,setLangua] = useState(language)
useEffect(() => {
let language = ""
if (typeof window !== 'undefined') {
if ( localStorage.getItem("language") === null) {
language = "english"
}
if ( localStorage.getItem("language") !== null) {
language = localStorage.getItem("language")
}
}
setLanguage(language)
}, [])
You can setLangua at the same time as putting it in local storage. Or you can subscribe to local storage changes with the useEffect hook.
import { useCallback, useEffect, useState } from 'react'
const [userLang, setUserLang] = useState('english')
const getLangFromLocalStorage = useCallback(() => {
return localStorage.getItem('userLang');
}, []);
useEffect(() => {
function checkUserLang() {
const value = getLangFromLocalStorage()
// Do with value what you want
if (value) {
setUserLang(value)
}
}
window.addEventListener('storage', checkUserLang)
return () => {
window.removeEventListener('storage', checkUserLang)
}
}, [])
// Set userLang initially when component did mount
useEffect(() => {
const value = getLangFromLocalStorage();
if (value) {
setUserLang(value);
}
}, []);
Note: This won't work on the same page that is making the changes — it is really a way for other pages on the domain using the storage to sync any changes that are made. Pages on other domains can't access the same storage objects.
Related
The useEffect doesn't fire on first render, but when I save the file (ctrl+s), the state updates and the results can be seen.
What I want to do is, when I'm in GameScreen, I tap on an ICON which takes me to WalletScreen, from there I can select some items/gifts (attachedGifts - in context) and after finalising I go back to previous screen i.e. GameScreen with gifts attached (attachedGifts!==null), now again when I tap ICON and go to WalletScreen it should show me the gifts that were attached so that I could un-attach them or update selection (this is being done in the useEffect below in WalletScreen), but the issue is, although my attachedGifts state is updating, the useEffect in WalletScreen does not fire immediately when navigated, when I hit ctrl+s to save the file, then I can see my selected/attached gifts in WalletScreen.
code:
const Main = () => {
return (
<GiftsProvider>
<Stack.Screen name='WalletScreen' component={WalletScreen} />
<Stack.Screen name='GameScreen' component={GameScreen} />
</GiftsProvider>
)
};
const GameScreen = () => {
const { attachedGifts } = useGifts(); //coming from context - GiftsProvider
console.log('attached gifts: ', attachedGifts);
return ...
};
const WalletScreen = () => {
const { attachedGifts } = useGifts();
useEffect(() => { // does not fire on initial render, after saving the file, then it works.
if (attachedGifts !== null) {
let selectedIndex = -1
let filteredArray = data.map(val => {
if (val.id === attachedGifts.id) {
selectedIndex = walletData.indexOf(val);
setSelectedGiftIndex(selectedIndex);
return {
...val,
isSelect: val?.isSelect ? !val?.isSelect : true,
};
} else {
return { ...val, isSelect: false };
}
});
setData(filteredArray);
}
}, [attachedGifts]);
const attachGiftsToContext = (obj) => {
dispatch(SET_GIFTS(obj));
showToast('Gifts attached successfully!');
navigation?.goBack(); // goes back to GameScreen
}
return (
// somewhere in between
<TouchableOpacity onPress={attachGiftsToContext}>ATTACH</TouchableOpacity>
)
};
context:
import React, { createContext, useContext, useMemo, useReducer } from 'react';
const GiftsReducer = (state: Object | null, action) => {
switch (action.type) {
case 'SET_GIFTS':
return action.payload;
default:
return state;
}
};
const GiftContext = createContext({});
export const GiftsProvider = ({ children }) => {
const initialGiftState: Object | null = null;
const [attachedGifts, dispatch] = useReducer(
GiftsReducer,
initialGiftState,
);
const memoedValue = useMemo(
() => ({
attachedGifts,
dispatch,
}),
[attachedGifts],
);
return (
<GiftContext.Provider value={memoedValue}>
{children}
</GiftContext.Provider>
);
};
export default function () {
return useContext(GiftContext);
}
Output of console.log in GameScreen:
attached gifts: Object {
"reciptId": "baNlCz6KFVABxYNHAHasd213Fu1",
"walletId": "KQCqSqC3cowZ987663QJboZ",
}
What could possibly be the reason behind this and how do I solve this?
EDIT
Added related code here: https://snack.expo.dev/uKfDPpNDr
From the docs
When you call useEffect in your component, this is effectively queuing
or scheduling an effect to maybe run, after the render is done.
After rendering finishes, useEffect will check the list of dependency
values against the values from the last render, and will call your
effect function if any one of them has changed.
You might want to take a different approach to this.
There is not much info, but I can try to suggest to put it into render, so it might look like this
const filterAttachedGifts = useMemo(() => ...your function from useEffect... , [attachedGitfs])
Some where in render you use "data" variable to render attached gifts, instead, put filterAttachedGifts function there.
Or run this function in component body and then render the result.
const filteredAttachedGifts = filterAttachedGifts()
It would run on first render and also would change on each attachedGifts change.
If this approach doesn't seems like something that you expected, please, provide more code and details
UPDATED
I assume that the problem is that your wallet receive attachedGifts on first render, and after it, useEffect check if that value was changed, and it doesn't, so it wouldn't run a function.
You can try to move your function from useEffect into external function and use that function in 2 places, in useEffect and in wallet state as a default value
feel free to pick up a better name instead of "getUpdatedArray"
const getUpdatedArray = () => {
const updatedArray = [...walletData];
if (attachedGifts !== null) {
let selectedIndex = -1
updatedArray = updatedArray.map((val: IWalletListDT) => {
if (val?.walletId === attachedGifts?.walletIds) {
selectedIndex = walletData.indexOf(val);
setSelectedGiftIndex(selectedIndex);
setPurchaseDetailDialog(val);
return {
...val,
isSelect: val?.isSelect ? !val?.isSelect : true,
};
} else {
return { ...val, isSelect: false };
}
});
}
return updatedArray;
}
Then use it here
const [walletData, setWalletData] = useState(getUpdatedArray());
and in your useEffect
useEffect(() => {
setWalletData(getUpdatedArray());
}, [attachedGifts]);
That update should cover the data on first render. That might be not the best solution, but it might help you. Better solution require more code\time etc.
My goal is to build a simple product review system using React, Next.JS and the browser's sessionStorage.
The user should be able to click on a button to "Add a review". This action will trigger the display of a text area and a submit button. Once the user click the submit button, the review content should be persisted in the sessionStorage and immediately showed up in a list of reviews.
My problem is that although I can update the sessionStorage after submitting the review, the app is not displaying the list of existing reviews right away.
If I leave the page and get back, the reviews will be shown up, meaning my custom hook seems to be working fine.
Here's the ReviewForm.tsx code:
export const ReviewForm: React.FC<Props> = ({ productId }): JSX.Element => {
const [showForm, setShowForm] = useState<boolean>(false);
const [storedValues, setStoredValues] = useSessionStorage<SessionStorage[]>(
"products-reviews",
[]
);
const registerReview = (event: any) => {
event.preventDefault();
const reviewText = event.target.review.value;
const productIndex = storedValues?.findIndex(
(review) => review.productId === productId
);
if (productIndex === -1 || productIndex === undefined) {
setStoredValues([...storedValues!, { productId, reviews: [reviewText] }]);
} else {
const reviews = [...storedValues![productIndex].reviews, reviewText];
const updatedReviews = [...storedValues!];
updatedReviews[productIndex].reviews = reviews;
setStoredValues(updatedReviews);
}
setShowForm(false);
};
return (
<div className={styles.reviewsContainer}>
<button
className={styles.addReviewButton}
onClick={() => setShowForm(true)}
>
<span>Add a review</span>
</button>
{showForm && (
<form
className={styles.reviewForm}
onSubmit={(event) => registerReview(event)}
>
<textarea className={styles.reviewInput} name="review" required />
<button className={styles.reviewSubmitButton} type="submit">
Submit
</button>
</form>
)}
<ReviewList productId={productId} />
</div>
);
};
And here's the ReviewList.tsx component, rendered inside ReviewForm.tsx:
export const ReviewList: React.FC<Props> = ({ productId }): JSX.Element => {
const [reviews, _] = useSessionStorage<SessionStorage[]>(
"products-reviews",
[]
);
const productReviews = reviews?.find(
(review) => review.productId === productId
)?.reviews;
return (
<ul>
{productReviews?.map((review) => (
<li key={Math.random() * 10000}>{review}</li>
))}
</ul>
);
};
Lastly, here's my custom hook useSessionStorage:
export const useSessionStorage = <T>(
key: string,
initialValue?: T
): SessionStorage<T> => {
const [storedValue, setStoredValue] = useState<T | undefined>(() => {
if (!initialValue) return;
try {
const value = sessionStorage.getItem(key);
return value ? JSON.parse(value) : initialValue;
} catch (error) {
return initialValue;
}
});
useEffect(() => {
if (storedValue) {
try {
sessionStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.log(error);
}
}
}, [storedValue, key]);
return [storedValue, setStoredValue];
};
The title of my question says "how to rerender child component" because I noticed if I completely delete the ReviewList.tsx component, bringing all its render logic inside the ReviewForm.tsx, my application will behave as expected.
So maybe the problem is related with this relation between components?
Any advice is welcome.
The problem
The problem is in your useSessionStorage hook. It is not actually synchronized with the session storage, because the state is actually stored with useState, it is only populated on mount.
How does it work in your case:
You initialize FIRST STATE using useState (inside custom useSessionStorage hook) with current session storage value on component mount at ReviewList.tsx
You initialize SECOND STATE using useState (inside custom useSessionStorage hook) with current session storage value on component mount at ReviewForm.tsx
You mutate SECOND STATE and push the changes to session storage with useEffect
So FIRST STATE is not updated with the new value until you re-mount the component.
Solution 1 (Will work only for sync between different browser tabs)
We need to reverse the flow of data from useState -> sessionStorage to sessionStorage -> useState
export const useSessionStorage = <T>(
key: string,
initialValue?: T
): SessionStorage<T> => {
const [storedValue, setStoredValue] = useState<T | undefined>(() => {
if (!initialValue) return;
try {
const value = sessionStorage.getItem(key);
return value ? JSON.parse(value!) : initialValue;
} catch (error) {
return initialValue;
}
});
const setStorageValue = useCallback((newValue: T) => {
try {
sessionStorage.setItem(key, JSON.stringify(newValue));
} catch (error) {
console.log(error);
}
}, []);
/** This `useEffect` will make sure `storedValue` is always in sync with the `sessionStorage` */
useEffect(() => {
const listenToStorageEvent = (event: StorageEvent) => {
if (event.storageArea === sessionStorage && event.key === key) {
try {
const newValue = JSON.parse(event.newValue!);
if (storedValue !== newValue) {
setStoredValue(newValue);
}
} catch (error) {
console.log(error);
}
}
};
window.addEventListener("storage", listenToStorageEvent);
return () => {
window.removeEventListener("storage", listenToStorageEvent);
};
}, [key]);
// We expose `setStorageValue` which works with `sessionStorage` instead of `setStoredValue` which works with local state
return [storedValue, setStorageValue];
};
Solution 2
Use custom events to be able to sync the same tab too
https://github.com/imbhargav5/rooks/blob/main/src/hooks/useSessionstorageState.ts
Solution 3
Parse the whole session storage on application start and put it as a state into a context. After that, on each "set" update both the context state and the sessionStorage. This solution has a lot of disadvantages like error proneness due to manual state to session storage synchronization, excessive re-rendering of the whole component tree under session storage provider on each storage value update. So I will not even add code examples here.
So I'm trying to check if localStorage is empty or not and if it's then I want my state to be changed immediately.
I've created this state and I'm setting this state initially empty
const [isLoggedIn, setLogin] = useState();
Then I'm using useEffect hook to get item from localStorage if something has in localStorage it will set my state true otherwise false
useEffect(() => {
if (localStorage.getItem("info") !== null) {
setLogin(true);
} else {
setLogin(false);
}
}, []);
So, when I add something or delete from my localStorage it doesn't work and I need refresh my page in order to make it work.
I've created a code sandbox to check what's going on, please have a look and help to make it work.
https://codesandbox.io/s/great-mclaren-vypyo?file=/src/App.js:164-304
Your localStorage commands look correct. But the issue is that you are not checking the storage everytime. You should have a state variable which you can use to indicate that something has changed and the storage needs to be checked again.
import "./styles.css";
import React, { useState, useRef, useEffect } from "react";
export default function App() {
const [isLoggedIn, setLogin] = useState();
const [change, setChange] = useState(false);
useEffect(() => {
if (localStorage.getItem("info") !== null) {
setLogin(true);
} else {
setLogin(false);
}
}, [change]);
console.log(isLoggedIn);
return (
<div className="App">
{isLoggedIn ? (
<h1>LocalStorage has something</h1>
) : (
<h2>LocalStorage has nothing</h2>
)}
<button
onClick={() => {
localStorage.setItem("info", "true");
setChange((change) => !change);
}}
>
Add to Local
</button>
<button
onClick={() => {
localStorage.removeItem("info");
setChange((change) => !change);
}}
>
Remove to Local
</button>
</div>
);
}
In the above code, change is changed when one of your storage command runs. You could have had a counter or anything else too. The main purpose is to check the storage everytime you are chaning stuff. This way you can have multiple storage commands (even more than 2), and then you can check your storage after every change.
link
Another simple approach could be to simply set the logged in state inside your functions where you run localStorage commands :
import "./styles.css";
import React, { useState, useRef, useEffect } from "react";
export default function App() {
const [isLoggedIn, setLogin] = useState();
useEffect(() => {
if (localStorage.getItem("info") !== null) {
setLogin(true);
} else {
setLogin(false);
}
}, [isLoggedIn]);
console.log(isLoggedIn);
return (
<div className="App">
{isLoggedIn ? (
<h1>LocalStorage has something</h1>
) : (
<h2>LocalStorage has nothing</h2>
)}
<button
onClick={() => {
localStorage.setItem("info", "true");
setLogin(true);
}}
>
Add to Local
</button>
<button
onClick={() => {
localStorage.removeItem("info");
setLogin(false);
}}
>
Remove to Local
</button>
</div>
);
}
Now you have one lesser state variable, but you will be directly manipulating the logged in state from your buttons.
link
If you don't setItem("info") I mean If "info" does not exist in localStorage then you'll get "null" and your code will work. But If you set something to "info" in localStorage for example if you do localStorage.setItem(""), then you won't get null, you'll get empty string. Therefore your code should be:
useEffect(() => {
if (localStorage.getItem("info") !== null || localStorage.getItem("info") !== "") {
setLogin(true);
} else {
setLogin(false);
}
}, []);
if you use array instead of string in "info" then
useEffect(() => {
if (localStorage.getItem("info") !== null || JSON.parse(localStorage.getItem("info")).length > 0) {
setLogin(true);
} else {
setLogin(false);
}
}, []);
The problem is that on the load of the page, you are setting the existence/non-existence of the localStorage item in the state of the component. However, when an action (setItem/removeItem) is performed, you are not updating the state. The simple way to accomplish it is to update the state in the event listener itself.
<button onClick={() => {
localStorage.setItem("info", "true");
setLogin(true);
}}>Add to Local</button>
<button onClick={() => {
localStorage.removeItem("info");
setLogin(false);
}}>Remove to Local</button>
Here is an updated, forked sandbox link.
You can use qs.
Then localStorage.setItem("info", qs.stringify(your_value_you_want_to_store));
if (!qs.parse(localStorage.getItem("info"))) {
setLogin(false);
} else {
setLogin(true);
}
I added an example here.
What is going on is that, the localstorage isnt reactive, and therefore you cannot listen to changes on it directl. you will need a mechanism to listen to changes. therefore i created a custom hook, that can get and set local storage values, and i used that instead.
import { useState, useEffect } from "react";
function getStorageValue(key, defaultValue) {
// getting stored value
const saved = localStorage.getItem(key);
const initial = JSON.parse(saved);
return initial || defaultValue;
}
export const useLocalStorage = (key, defaultValue) => {
const [value, setValue] = useState(() => {
return getStorageValue(key, defaultValue);
});
useEffect(() => {
// storing input name
console.log("called", value);
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
};
Now if you set new changes, you will get an updated state. Fell free to extend it however you like.
LocalStorage isn't reactive, it doesn't signal back to react components when it's updated.
You should initialize your state from localStorage, and persist state update to localStorage. The button click handlers should only update the local isLoggedIn state.
Use a lazy state initializer function to read from localStorage and return the initial state value.
Use an useEffect hook to persist the state to localStorage.
button onClick handlers update local state.
Code:
function App() {
const [isLoggedIn, setLogin] = useState(() =>
JSON.parse(localStorage.getItem("info"))
);
useEffect(() => {
localStorage.setItem("info", JSON.stringify(isLoggedIn));
}, [isLoggedIn]);
console.log(isLoggedIn);
return (
<div className="App">
{isLoggedIn ? (
<h1>LocalStorage has something</h1>
) : (
<h2>LocalStorage has nothing</h2>
)}
<button onClick={() => setLogin(true)}>Add to Local</button>
<button onClick={() => setLogin(false)}>Remove to Local</button>
</div>
);
}
i want to show a dialog on clicking add button. and not display it for the session (using sessionstorage) when hide button is clicked.
below is my code,
function Parent() {
const DialogRequest = useDialogRequest();
const onAdd = () => {
DialogRequest(true);
}
render = () => {
return (
<button onClick={onAdd}>Add</button>
);
}
}
function Dialog(onHide) {
return(
{showDialog?
hide : null
}
);
}
const dialogRequestContext = React.createContext<ContextProps>({});
export const DialogRequestProvider = ({ children }: any) => {
const [showDialog,setShowDialog] = React.useState(false);
const onHide = () => {
setDialogOpen(false);
};
const setDialogOpen = (open: boolean) => {
if (open) {
const sessionDialogClosed = sessionStorage.getItem('dialog');
if (sessionDialogClosed !== 'closed') {
setShowDialog(open);
sessionStorage.setItem('dialog', 'closed');
}
} else {
setShowDialog(open);
}
};
return (
<DialogContext.Provider
value={{ setDialogOpen }}
>
{children}
<Dialog onHide={onHide}
showDialog={showDialog}
/>
</DialogContext.Provider>
);
};
export function useDialogRequest() {
const dialogRequestContext = React.useContext(
dialogRequestContext
);
return function DialogRequest(open: boolean) {
if (dialogRequestContext &&
dialogRequestContext.setDialogOpen
) {
dialogRequestContext.setDialogOpen(open);
}
};
}
This code works.but when page reloads then the dialog is not opened again even though hide message is not clicked before page reload.
i have tried to console log the value of dialog like below after page reload.
if (open) {
const sessionDialogClosed = sessionStorage.getItem('dialog');
console.log('sessiondialogclosed', sessionDialogClosed); //this gives closed
if (sessionDialogClosed !== 'closed') {
setShowDialog(open);
sessionStorage.setItem('dialog', 'closed');
}
} else {
setShowDialog(open);
}
Even though i dint click the hide button before page reload.....this gives me the ouput closed for the sessionstorage item dialog.
Not sure if this is the way it should behave. If not could someone help me fix this to get it right.
thanks.
As expressed in my comment:
Values in useState persist for the life of the page. They won't be kept across a page refresh. If you need to do that, you might consider creating a hook that wraps useState which persists the values into localStorage, and then retrieves the initial value on page load.
This is a basic example of such a hook.
function usePersistentState(defaultValue, key) {
let initVal;
try {
initVal = JSON.parse(localStorage.getItem(key));
} catch {}
if (initVal === undefined) initVal = defaultValue;
const [state, setState] = useState(initVal);
function persistState(value) {
if (typeof value === "function") value = value(state);
localStorage.setItem(key, JSON.stringify(value));
setState(value);
}
return [state, persistState];
}
Here's a sample of how it works. If you refresh the page after toggling the value, it will restore the previous state from localStorage.
There are a few caveats about how it works. If you've got a really complex state you shouldn't use this, you should do basically the same thing but with useReducer instead. But in a simple example like this should be fine.
In my react app i'm saving user data filters into localStorage. I want to useEffect, when that data was changed. How to correctly trigger that effect? I tried this:
useEffect(() => {
if (rawEstateObjects.length && localStorage.activeEstateListFilter) {
const activeFilter = JSON.parse(localStorage.activeEstateListFilter)
if (activeFilter.length) {
applyFilter(activeFilter)
}
}
}, [rawEstateObjects, localStorage.activeEstateListFilter])
but localStorage.activeEstateListFilter doesn't triggers the effect..
did you set value in localStorge?
if set it then:
useEffect(() => {
function activeEstateList() {
const item = localStorage.getItem('activeEstateListFilter')
if (rawEstateObjects.length && item) {
const activeFilter = JSON.parse(localStorage.activeEstateListFilter)
if (activeFilter.length) {
applyFilter(activeFilter)
}
}
}
window.addEventListener('storage', activeEstateList)
return () => {
window.removeEventListener('storage', activeEstateList)
}
}, [])
refrence answer: https://stackoverflow.com/a/61178371/7962191