On page refresh, how does the session storage work? - javascript

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.

Related

How to change state when localStorage value changed in Next.js?

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.

context api - useEffect doesn't fire on first render - react native

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.

How to immediately rerender child component after updating the sessionStorage using custom hook

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.

Show alert if state is true and if localStorage is true, remove localStorage if browser refresh

I'm trying to display alert message if the state is true. Also I'm trying to save that state in localStorage and everytime when localStorage is true to display the alert.
On Page refresh, I'm trying to delete the localStorage.
import React, {
useEffect, useState
} from 'react';
const MainClass = () => {
const [alert, setAlert] = useState(false);
useEffect(() => {
setAlert(true);
if (alert === true) {
localStorage.setItem('Alert', alert);
}
}, []);
const test = localStorage.getItem('Alert');
window.onload = function () {
console.log('TEST on load');
localStorage.removeItem('Alert');
};
return(
{alert && test === true && (
<Alert
message="Messgae"
/>
)}
)
};
My issue is that I'm not able to display the alert if I add the condition test === true and also I'm not able to remove the localStorage on browser refresh
no need to use === just if (alert) works to check for a truthy. Note that what
useEffect(() => {
setAlert(true);
if (alert === true) {
localStorage.setItem('Alert', alert);
}
}, []);
this basically sets alert true upon rendering the component. Are you sure that this is what you are looking for?

Redirect to the login page in React

I have a redux store a the user that is currently logged in. When I refresh the page the state is lost, and want I want to do is when I refresh the any component of my I want to be redirected to the loggin page,how can I do that?
I am using LocalStorage when I make a redirect about login.
I just stored login data to LocalStorage.
Main Page Component
import React from 'react';
import { Redirect } from 'react-router-dom';
import storage from 'lib/storage';
const MainPage = () => {
function redirect(){
if(storage.get('user')){
return <Redirect to="/login" />
}else{
return null;
}
}
return (
<main>
{redirect()}
/* ... */
</main>
)
}
lib/storage.js
export default (function () {
const st = localStorage || {};
return {
set: (key, object) => {
const arr = [];
arr.push(JSON.stringify(object));
st[key] = arr;
},
add: (key, object) => {
let old = st.getItem(key);
if (old === null) old = '';
st.setItem(key, old + ((typeof object) === 'string' ? object : JSON.stringify(object)));
},
get: (key) => {
if (!st[key]) {
return null;
}
const value = st[key];
try {
const parsed = JSON.parse(value);
return parsed;
} catch (e) {
return value;
}
},
remove: (key) => {
if (localStorage) {
return localStorage.removeItem(key);
}
delete st[key];
}
}
})();
Your Store state will be lost whenever you refresh the page. To persist this state, assuming that you are developing a web application, you must use one of the browser storage options to save the user̈́'s logged state.
Short Example:
// Setting the user info to the local storage
localStorage.setItem('userId', 'Logged');
// Retrieving the information before rendering the route component
var userInfo = localStorage.getItem('userId');
Please, refer for this link to a full example.
Obs: This is a JavaScript problem, not a React.js problem

Categories

Resources