最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to instantly update state when any changes into the localStorage in React.js - Stack Overflow

programmeradmin1浏览0评论

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.

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.

Share Improve this question edited Jul 14, 2020 at 14:02 Apostolos 10.5k5 gold badges31 silver badges44 bronze badges asked Jul 14, 2020 at 14:00 jesicajesica 6852 gold badges14 silver badges38 bronze badges 5
  • 1 so how localStorage changes are made? – Apostolos Commented Jul 14, 2020 at 14:03
  • why linking the state with window storage? – Muhammed Moussa Commented Jul 14, 2020 at 14:04
  • @Apostolos Adding a new item to the cart or updating quantity. – jesica Commented Jul 14, 2020 at 14:04
  • Can you show the function which use to add items to cart ? – Dilshan Commented Jul 14, 2020 at 14:08
  • Stop making unnecessary side effects and trying to trigger updates according to them. Just trigger your update right after you updated local storage. If it's not in the same component, use React Context to share state between components. – Hamed Siaban Commented May 26, 2024 at 16:22
Add a comment  | 

8 Answers 8

Reset to default 15

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

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;

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>

}
发布评论

评论列表(0)

  1. 暂无评论