How to get updated state from Redux store using redux-toolkit after component has already rendered?

9.3k views Asked by At

The functionality I am looking for is that of an e-commerce website. A click on the add-to-cart button adds product to cart. I am able to successfully achieve that by dispatching an action. Post that I want to grab the state of the updated cart without having to re-render the component and save it to localStorage. Since the component does not re-render on its own, the value of cartItems fetched by useSelector on the initial render is not the updated one and hence always is one update behind. I am relatively new to redux and completely new to redux-toolkit. With my research so far, I figured out that getState() and redux-thunk may be of help, but didn't seem to get a good code example.

this is the cartSlice

const cartSlice = createSlice({
  name: "cart",
  initialState,
  reducers: {
    // Todo: add pizza to cart
    addItemToCart: (state, { payload }) => {
      const newItem = payload;
      const existingItem = state.cartItems.find(
        (item) => item.id === newItem.id && item.varient === newItem.varient
      );

      if (existingItem) {
        existingItem.quantity = existingItem.quantity + newItem.quantity;
        existingItem.price =
          existingItem.quantity * existingItem.prices[0][existingItem.varient];
      } else {
        state.cartItems.push(newItem);
      }
    },

And the add to cart function

  const [quantity, setQuantity] = useState(1);
  const cartItems = useSelector((state) => state.cart.cartItems);
  const dispatch = useDispatch();

  const handleAddToCart = (pizza, varient, quantity) => {
    let item = {
      id: pizza._id,
      name: pizza.name,
      image: pizza.image,
      varient,
      quantity,
      price: pizza.prices[0][varient] * quantity,
      prices: pizza.prices,
    };
    dispatch(addItemToCart(item));
    dispatch(setTotalPrice());
    dispatch(setCartCount());

    // i want to grab the current state here after it has been updated in line 27 and save it in local storage
  };```
2

There are 2 answers

0
Cesare Polonara On

I think a good approach is to make a custom hook like this:

    const useSaveToLocalstorage = (trigger, data) => {
      useEffect(() => {
        if (data) window.localStorage.setItem(data.key, data.value);
      }, [trigger]);
};

And to call it like this (Your code):

const [quantity, setQuantity] = useState(1);
const cartItems = useSelector((state) => state.cart.cartItems);
const dispatch = useDispatch();
useSaveToLocalStorage(state.cart.cartItems, { key: 'cart.items',
                                                  value: state.cart.cartItems });

const handleAddToCart = (pizza, varient, quantity) => {
  let item = {
    id: pizza._id,
    name: pizza.name,
    image: pizza.image,
    varient,
    quantity,
    price: pizza.prices[0][varient] * quantity,
    prices: pizza.prices,
  };
  dispatch(addItemToCart(item));
  dispatch(setTotalPrice());
  dispatch(setCartCount());

  // i want to grab the current state here after it has been updated in line 27 and save it in local storage
};

You can't access the updated state after a direct action dispatch, since the store update is an asynchronous task, but you can react to store slice change, by using a useEffect with that state slice as dependency.

1
Karthik Sivasubramaniam On

The problem

You do not exactly need a re-render to happen for accessing the latest state from your store. Your state is one update behind, because you're trying to fetch values from useSelector right after your dispatch calls. The dispatch calls are asynchronous (but there is no promise to "await" them by default), so your state is not guaranteed to be up-to-date by the time your selector call goes through.

Solution

Move your selector call into a separate useEffect hook that is triggered every time cartItems changes.

useEffect(() => {
    if (cartItems) {
      // Do your thing here
    }
}, [cartItems]); // Guaranteed to be triggered *after* each update

Possible Alternative

I have read that you could await the dispatch calls using createAsyncThunk, but I have not personally tried it. Given your requirement here, though, I don't think you need to go that far. Here is the documentaion for createAsyncThunk, if you're interested: https://redux-toolkit.js.org/api/createAsyncThunk