简体   繁体   中英

How to instantly update state when any changes into the localStorage in React.js

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.

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.

.

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,

  1. first load data from localstorage / database and initialize the state.
  2. Then update the state in the code as you want.
  3. Then listen on state changes to do any task ( In this case save to localstorage )

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

  1. if the component that has addCart function is a child component of cart component, use props - https://reactjs.org/docs/components-and-props.html
  2. if the component that has addCart function is NOT a child component of cart component, use context api to communicate between components - https://reactjs.org/docs/context.html
  3. if you want to manage your data in clean & better way, use https://redux.js.org/

According 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

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

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)
        }
    }, [])

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.

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>
        );
    }

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>

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM