Advanced Redux
๊ณ ๊ธ ๋ฆฌ๋์ค
Udemy ๊ฐ์ ๋ด์ฉ์ ๋ฐ๋ผํด ๋ณธ ๊ธ์
๋๋ค. github ๋ฆฌํฌ์งํ ๋ฆฌ์ 19-advanced-redux
๋ธ๋์น๋ฅผ ์ด์ฉํ์ฌ ๋ฐ๋ผ ํด ๋ณผ ์ ์์ต๋๋ค.
- Handling Async tasks with redux
- Where to put our code
- The Redux devtools
Install
yarn add @reduxjs/toolkit
yarn add react-redux
Setting up store and uiSlice
- Add store folder and index.js file & uiSlice.js file
// store/index.js
import { configureStore } from "@reduxjs/toolkit";
import uiSlice from "./uiSlice"l
const store = configureStore({
reducer: {
ui: uiSlice.reducer
}
})
export default store
// store/ui-slice.js
import { createSlice } from "@reduxjs/toolkit";
const uiSlice = createSlice({
name: "ui",
initialState: { cartIsVisible: false },
reducers: {
toggle(state) {
state.cartIsVisible = !state.cartIsVisible;
},
},
});
export const uiActions = uiSlice.actions;
export default uiSlice;
- Setting up Provider and store
// src/index.js
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import store from "./store";
import "./index.css";
import App from "./App";
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
Use uiSlice
- Button clickํ๋ฉด ํ ๊ธํ๋ ๋ก์ง ์ถ๊ฐ
// src/components/Cart/CartButton.js
import { useDispatch } from "react-redux";
import { uiActions } from "../../store/ui-slice";
import classes from "./CartButton.module.css";
const CartButton = (props) => {
const dispatch = useDispatch();
const toggleCartHandler = () => {
dispatch(uiActions.toggle());
};
return (
<button className={classes.button} onClick={toggleCartHandler}>
<span>My Cart</span>
<span className={classes.badge}>1</span>
</button>
);
};
export default CartButton;
- state์ ๋ฐ๋ผ ์ปดํฌ๋ํธ๋ฅผ ๋ณด์ฌ์ฃผ๊ธฐ
// src/App.js
import { useSelector } from "react-redux";
import Cart from "./components/Cart/Cart";
import Layout from "./components/Layout/Layout";
import Products from "./components/Shop/Products";
function App() {
const showCart = useSelector((state) => state.ui.cartIsVisible);
return (
<Layout>
{showCart && <Cart />}
<Products />
</Layout>
);
}
export default App;
Setting up cartSlice
// store/index.js
import { configureStore } from "@reduxjs/toolkit";
import cartSlice from "./cart-slice";
import uiSlice from "./ui-slice";
const store = configureStore({
reducer: {
ui: uiSlice.reducer,
cart: cartSlice.reducer,
},
});
export default store;
// store/cart-slice.js
import { createSlice } from "@reduxjs/toolkit";
const cartSlice = createSlice({
name: "cart",
initialState: {
items: [],
totalQuantity: 0,
},
reducers: {
addItemToCart(state, action) {
const newItem = action.payload;
const existingItem = state.items.find((item) => item.id === newItem.id);
state.totalQuantity++;
if (!existingItem) {
state.items.push({
id: newItem.id,
price: newItem.price,
quantity: 1,
totalPrice: newItem.price,
title: newItem.title,
});
} else {
existingItem.quantity++;
existingItem.totalPrice += newItem.price;
}
},
removeItemFromCart(state, action) {
const id = action.payload;
const existingItem = state.items.find((item) => item.id === id);
state.totalQuantity--;
if (existingItem.quantity === 1) {
state.items = state.items.filter((item) => item.id !== id);
} else {
existingItem.quantity--;
existingItem.totalPrice -= existingItem.price;
}
},
},
});
export const cartActions = cartSlice.actions;
export default cartSlice;
Use cartSlice
- Add dummy data
// src/components/Shop/Product.js
import ProductItem from "./ProductItem";
import classes from "./Products.module.css";
const DUMMY_PRODUCT = [
{
id: "p1",
title: "first book",
price: 6,
description: "this is my first book",
},
{
id: "p2",
title: "2nd book",
price: 4,
description: "this is my 2nd book",
},
];
const Products = (props) => {
return (
<section className={classes.products}>
<h2>Buy your favorite products</h2>
<ul>
{DUMMY_PRODUCT.map((product) => (
<ProductItem
key={product.id}
id={product.id}
title={product.title}
price={product.price}
description={product.description}
/>
))}
</ul>
</section>
);
};
export default Products;
- Use cartActions on button click
// src/components/Shop/ProductItem.js
import { useDispatch } from "react-redux";
import { cartActions } from "../../store/cart-slice";
import Card from "../UI/Card";
import classes from "./ProductItem.module.css";
const ProductItem = (props) => {
const dispatch = useDispatch();
const { id, title, price, description } = props;
const addToCartHandler = () => {
dispatch(
cartActions.addItemToCart({
id,
title,
price,
description,
})
);
};
return (
<li className={classes.item}>
<Card>
<header>
<h3>{title}</h3>
<div className={classes.price}>${price.toFixed(2)}</div>
</header>
<p>{description}</p>
<div className={classes.actions}>
<button onClick={addToCartHandler}>Add to Cart</button>
</div>
</Card>
</li>
);
};
export default ProductItem;
- Add real data on Cart
// src/components/Cart/Cart.js
import { useSelector } from "react-redux";
import Card from "../UI/Card";
import classes from "./Cart.module.css";
import CartItem from "./CartItem";
const Cart = (props) => {
const cartItems = useSelector((state) => state.cart.items);
return (
<Card className={classes.cart}>
<h2>Your Shopping Cart</h2>
<ul>
{cartItems.map((item) => (
<CartItem
key={item.id}
item=
/>
))}
</ul>
</Card>
);
};
export default Cart;
- Add handlers for buttons
// src/components/Cart/CartItem.js
import { useDispatch } from "react-redux";
import classes from "./CartItem.module.css";
import { cartActions } from "../../store/cart-slice";
const CartItem = (props) => {
const { id, title, quantity, total, price } = props.item;
const dispatch = useDispatch();
const removeItemHandler = () => {
dispatch(cartActions.removeItemFromCart(id));
};
const addItemHandler = () => {
dispatch(
cartActions.addItemToCart({
id,
title,
quantity,
total,
price,
})
);
};
return (
<li className={classes.item}>
<header>
<h3>{title}</h3>
<div className={classes.price}>
${total.toFixed(2)}{" "}
<span className={classes.itemprice}>(${price.toFixed(2)}/item)</span>
</div>
</header>
<div className={classes.details}>
<div className={classes.quantity}>
x <span>{quantity}</span>
</div>
<div className={classes.actions}>
<button onClick={removeItemHandler}>-</button>
<button onClick={addItemHandler}>+</button>
</div>
</div>
</li>
);
};
export default CartItem;
Using useEffect for update data
// src/App.js
import { useEffect } from "react";
import { useSelector } from "react-redux";
import Cart from "./components/Cart/Cart";
import Layout from "./components/Layout/Layout";
import Products from "./components/Shop/Products";
function App() {
const showCart = useSelector((state) => state.ui.cartIsVisible);
const cart = useSelector((state) => state.cart);
useEffect(() => {
fetch("http://someAPI.com/cart.json", {
method: "PUT",
body: JSON.stringify(cart),
});
}, [cart]);
return (
<Layout>
{showCart && <Cart />}
<Products />
</Layout>
);
}
export default App;
Problem using useEffect
-
ํ์ฌ ์ฐ๋ฆฌ๊ฐ ์ฌ์ฉํ๋ ๋ฐฉ์์ผ๋ก useEffect๋ฅผ ์ฌ์ฉํ ๋ ํ ๊ฐ์ง ๋ฌธ์ ์ ์ง๋ฉดํฉ๋๋ค: ์ฑ์ด ์์๋ ๋ ๊ทธ๊ฒ์ด ์คํ๋๋ค๋ ๊ฒ์ ๋๋ค.
-
์ด๊ฒ์ด ์ ๋ฌธ์ ์ผ๊น์?
-
์ด๊ฒ์ ์ด๊ธฐ(์ฆ, ๋น์ด ์๋) ์นดํธ๋ฅผ ๋ฐฑ์๋๋ก ๋ณด๋ด๊ณ ๊ฑฐ๊ธฐ์ ์ ์ฅ๋ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ๋ฎ์ด์ฐ๊ธฐ ๋๋ฌธ์ ๋ฌธ์ ์ ๋๋ค.
Stopping sending data on initial mount & Using action creator
- Set up notification action
// store/ui-slice.js
import { createSlice } from "@reduxjs/toolkit";
const uiSlice = createSlice({
name: "ui",
initialState: { cartIsVisible: false, notification: null },
reducers: {
toggle(state) {
state.cartIsVisible = !state.cartIsVisible;
},
showNotification(state, action) {
state.notification = {
status: action.payload.status,
title: action.payload.title,
message: action.payload.message,
};
},
},
});
export const uiActions = uiSlice.actions;
export default uiSlice;
- using isInitial variable outside of component
// src/App.js
import { Fragment, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import Cart from "./components/Cart/Cart";
import Layout from "./components/Layout/Layout";
import Products from "./components/Shop/Products";
import Notification from "./components/UI/Notification";
import { sendCartData } from "./store/cart-slice";
let isInitial = true;
function App() {
const dispatch = useDispatch();
const showCart = useSelector((state) => state.ui.cartIsVisible);
const cart = useSelector((state) => state.cart);
const notification = useSelector((state) => state.ui.notification);
useEffect(() => {
if (isInitial) {
isInitial = false;
return;
}
dispatch(sendCartData(cart));
}, [cart, dispatch]);
return (
<Fragment>
{notification && (
<Notification
status={notification.status}
title={notification.title}
message={notification.message}
/>
)}
<Layout>
{showCart && <Cart />}
<Products />
</Layout>
</Fragment>
);
}
export default App;
- Add action creator in cart-slice
// src/store/cart-slice.js
import { createSlice } from "@reduxjs/toolkit";
import { uiActions } from "./ui-slice";
const cartSlice = createSlice({
name: "cart",
initialState: {
items: [],
totalQuantity: 0,
},
reducers: {
replaceCart(state, action) {
state.totalQuantity = action.payload.totalQuantity;
state.items = action.payload.items;
},
addItemToCart(state, action) {
const newItem = action.payload;
const existingItem = state.items.find((item) => item.id === newItem.id);
state.totalQuantity++;
if (!existingItem) {
state.items.push({
id: newItem.id,
price: newItem.price,
quantity: 1,
totalPrice: newItem.price,
name: newItem.title,
});
} else {
existingItem.quantity++;
existingItem.totalPrice = existingItem.totalPrice + newItem.price;
}
},
removeItemFromCart(state, action) {
const id = action.payload;
const existingItem = state.items.find((item) => item.id === id);
state.totalQuantity--;
if (existingItem.quantity === 1) {
state.items = state.items.filter((item) => item.id !== id);
} else {
existingItem.quantity--;
}
},
},
});
export const sendCartData = (cart) => {
return async (dispatch) => {
dispatch(
uiActions.showNotification({
status: "pending",
title: "Sending...",
message: "Sending cart data!",
})
);
const sendRequest = async () => {
const response = await fetch(
"https://react-http-6b4a6.firebaseio.com/cart.json",
{
method: "PUT",
body: JSON.stringify(cart),
}
);
if (!response.ok) {
throw new Error("Sending cart data failed.");
}
};
try {
await sendRequest();
dispatch(
uiActions.showNotification({
status: "success",
title: "Success!",
message: "Sent cart data successfully!",
})
);
} catch (error) {
dispatch(
uiActions.showNotification({
status: "error",
title: "Error!",
message: "Sending cart data failed!",
})
);
}
};
};
export const cartActions = cartSlice.actions;
export default cartSlice;