Reputation: 685
How to update cart page data instantly when any changes in the localStorage
myCart
array? Here is my code below
const [cart, setCart] = React.useState([])
React.useEffect(() => {
setCart(JSON.parse(localStorage.getItem('myCart')) || [])
}, [])
the cart
is updated when the page reloads but not when new item adds or updates any existing items!
How can I achieve that, which is cart
update instantly if any changes into 'localStorage`?
Thanks in advance.
Upvotes: 15
Views: 32869
Reputation: 346
Here's a hook I made for this. Inspired by https://michalkotowski.pl/writings/how-to-refresh-a-react-component-when-local-storage-has-changed
// Inspired by https://michalkotowski.pl/writings/how-to-refresh-a-react-component-when-local-storage-has-changed
const useLocalStorage = <T extends object>(key: string) => {
const [storage, _setStorage] = useState<T>({} as unknown as T);
useEffect(() => {
const handleStorage = () => {
_setStorage(JSON.parse(localStorage.getItem(key) ?? '{}'));
};
window.addEventListener('storage', handleStorage);
return () => window.removeEventListener('storage', handleStorage);
}, []);
const setStorage = (data: unknown) => {
localStorage.setItem(key, JSON.stringify(data));
window.dispatchEvent(new Event('storage'));
};
return [storage, setStorage];
};
export default useLocalStorage;
Upvotes: 1
Reputation: 791
Use react context provider together with local storage
type Foo = {
bar: string
}
const FooEmpty = {
bar: ""
}
const FOO_STORAGE_ITEM = "__foo";
export const removeFooStorage = () => {
localStorage.removeItem(FOO_STORAGE_ITEM);
};
export const getFooStorage = (): Foo => {
const fromStorage = localStorage.getItem(FOO_STORAGE_ITEM);
return fromStorage ? JSON.parse(fromStorage) : FooEmpty;
};
export const setFooStorage = (
foo: Foo
) => {
localStorage.setItem(
FOO_STORAGE_ITEM,
JSON.stringify(foo)
);
};
export const FooContext = React.createContext({
foo: FooEmpty,
setFoo: (value: Foo) => {},
});
Then wrap the components that need the storage item into context
const MyComponent = () => {
// Init foo state from storage
const [fooState, setFooState] = useState(
getFooStorage()
);
// Every time when context state update, update also storage item
useEffect(() => {
setFooStorage(fooState);
}, [fooState]);
// Wrap all foo state consumer components in Foo context provider
<FooContext.Provider
value={{
foo: fooState
setFoo: setFooState
}}
>
// In foo consumer component, call useContext(FooContext) to read or update foo states
<FooConsumerComponent/>
</FooContext.Provider>
}
Upvotes: 0
Reputation: 46
It's an old question but still seems popular so for anyone viewing now, since I'm not sure it's possible to trigger a real-time update from window storage and have it be reliable, I would suggest a reducer hook to solve the problem (you can update storage via the reducer but it would likely be redundant). This is a basic version of the useCart hook I used with useReducer to keep the cart state updated (updates in real-time when quantity changes as well).
import React, { useReducer, useContext, createContext, useCallback} from 'react';
const CartContext = createContext();
export function cartReducer(cartState, action) {
switch (action.type) {
case 'addToCart':
const newItem = action.payload;
const isExisting = cartState.find( item => (item.serviceid === newItem.serviceid));
if(isExisting){
return cartState.map( currItem => (currItem.serviceid === newItem.serviceid) ?
{...newItem, quantity: currItem.quantity + 1}
: currItem
)};
return [...cartState, {...newItem}]
case 'removeFromCart':
const itemToRemove = action.payload;
const existingCartItem = cartState.find(
cartItem => cartItem.serviceid === itemToRemove.serviceid
);
if(existingCartItem){
return cartState.map(cartItem =>
(cartItem.serviceid === itemToRemove.serviceid && cartItem.quantity > 0) ?
{...cartItem, quantity: cartItem.quantity - 1}
: cartItem
)}
return [...cartState, action.payload]
case 'emptyCart':
return []
default:
throw new Error();
}
}
export const CartProvider = ({ children }) => {
const [state, dispatch] = useReducer(cartReducer, []);
return <CartContext.Provider value={{ state, dispatch }}>{children}</CartContext.Provider>;
};
export const useCart = () => {
const {state, dispatch} = useContext(CartContext);
const addToCart = useCallback((payload) => {
dispatch({type: 'addToCart', payload});
}, [dispatch, state]);
const removeFromCart = useCallback((payload) => {
dispatch({type: 'removeFromCart', payload});
}, [dispatch, state]);
const emptyCart = useCallback((payload) => {
dispatch({type: 'emptyCart', payload});
}, [dispatch]);
return {
cart: state,
addToCart,
removeFromCart,
emptyCart,
}
}
Then all you need to do is either add it to your provider component (if you have a lot of providers) or just wrap your app in the CartProvider directly.
import React from "react";
import { AuthProvider } from "./hooks/useAuth";
import { CartProvider } from "./hooks/useCart";
import { PaymentProvider } from "./paymentGateway/gatewayHooks/usePayment.js";
import { StoreProvider } from "./store/StoreProvider";
import { MuiPickersUtilsProvider } from "@material-ui/pickers";
import DateFnsUtils from "@date-io/date-fns";
export function AppProvider({ children }) {
return (
<AuthProvider>
<StoreProvider>
<CartProvider>
<PaymentProvider>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
{children}
</MuiPickersUtilsProvider>
</PaymentProvider>
</CartProvider>
</StoreProvider>
</AuthProvider>
);
}
Your main App component would look something like this when you're done and you can also now access the cart state from any other child in your app regardless of how deep it may be buried.
export default function App() {
return (
<ThemeProvider theme={theme}>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<CssBaseline />
<BrowserRouter>
<AppProvider>
<AppLayout />
</AppProvider>
</BrowserRouter>
</LocalizationProvider>
</ThemeProvider>
);
}
Upvotes: 0
Reputation: 851
This is not intended to be an exact answer to the OP. But I needed a methed to update the State (from local storage) when the user changes tabs, because the useEffect was not triggered in the second tab, and therefore the State was outdated.
window.addEventListener("visibilitychange", function() {
setCart(JSON.parse(localStorage.getItem('myCart')))
})
This adds a listener which is triggered when the visibility changes, which then pulls the data from local storage and assigns it to the State.
In my application, triggering on a "storage" event listener resulted in this running far too many times, while this gets triggered less (but still when needed) - when the user changes tabs.
Upvotes: 0
Reputation: 20078
We can get live update using useEffect in the following way
React.useEffect(() => {
const handleExceptionData = () => {
setExceptions(JSON.parse(localStorage.getItem('exp')).data)
}
window.addEventListener('storage', handleExceptionData)
return function cleanup() {
window.removeEventListener('storage', handleExceptionData)
}
}, [])
Upvotes: 0
Reputation: 514
I followed the answer given by @Shubh
above.
It works when an existing value is deleted from the local storage or when another window in the same browser updates a value being used in the local storage. It does not work when, let's say, clicking on a button in the same window updates the local storage.
The below code handles that as well. It might look a bit redundant, but it works:
const [loggedInName, setLoggedInName] = useState(null);
useEffect(() => {
setLoggedInName(localStorage.getItem('name') || null)
window.addEventListener('storage', storageEventHandler, false);
}, []);
function storageEventHandler() {
console.log("hi from storageEventHandler")
setLoggedInName(localStorage.getItem('name') || null)
}
function testFunc() {
localStorage.setItem("name", "mayur1234");
storageEventHandler();
}
return(
<div>
<div onClick={testFunc}>TEST ME</div>
</div>
)
Edit:
I also found a bad hack to do this using a hidden button in one component, clicking which would fetch the value from localStorage and set it in the current component.
This hidden button can be given an id and be called from any other component using plain JS
eg: document.getElementById("hiddenBtn").click()
See https://stackoverflow.com/a/69222203/9977815 for details
Upvotes: 1
Reputation: 3001
I think this is a better way to handle the situation. This is how I handle a situation like this.
In you component,
const [cart, setCart] = useState([]);
React.useEffect(() => {
async function init() {
const data = await localStorage.getItem('myCart');
setCart(JSON.parse(data));
}
init();
}, [])
the cart is updated when the page reloads but not when new item adds or updates any existing items!
How can I achieve that, which is cart update instantly if any changes into 'localStorage`?
When you add items, let's assume you have a method addItem()
async function addItem(item) {
// Do not update local-storage, instead update state
await setState(cart => cart.push(item));
}
Now add another useEffect()
;
useEffect(() => {
localstorage.setItem('myCart', cart);
}, [cart])
Now when cart state change it will save to the localstorage
addEventListener
on storage is not a good thing. Everytime storage change, in you code, you have to getItems and parsed it which takes some milliseconds to complete, which cause to slow down the UI update.
In react,
localstorage
/ database
and initialize the state.If your addItem()
function is a child of the above component ( cart component ), then you can pass the setItem
funtion as a prop / or you can use contex API or else use Redux
UPDATE
If addCart
is another component
addCart
function is a child component of cart
component, use props
- https://reactjs.org/docs/components-and-props.htmladdCart
function is NOT a child component of cart
component, use context api to communicate between components - https://reactjs.org/docs/context.htmlAccording to the use case, I assume your addItem
function declared in a Product
component..
I recommend you to use Redux to handle the situation.
UPDATE
As @Matt Morgan said in comment section, using context API is better for the situation.
But if your application is a big one ( I assumed that you are building an e-commerce system ) it may be better to use a state management system like Redux. Just for this case, context API will be enough
Upvotes: 2
Reputation: 1
You can add an eventlistener to the localstorage
event
React.useEffect(() => {
window.addEventListener('storage', () => {
// When local storage changes, dump the list to
// the console.
setCart(JSON.parse(localStorage.getItem('myCart')) || [])
});
}, [])
The storage event of the Window interface fires when a storage area (localStorage) has been modified.The storage event is only triggered when a window other than itself makes the changes.
.
Upvotes: 15