Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useReducer #1

Closed
wants to merge 15 commits into from
56 changes: 9 additions & 47 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useReducer } from "react";
import "./App.css";
import Footer from "./Footer";
import Header from "./Header";
Expand All @@ -7,55 +7,17 @@ import { Route, Routes } from "react-router-dom";
import Detail from "./Detail";
import Cart from "./Cart";
import Checkout from "./Checkout";
import cartReducer from "./cartReducer";

function App() {
// Note, can call React.useState if you prefer
// Build up state slowly. Start with const statusState = useState(); Then destructure just first element in array. Then 2nd.
// Pass func so it's only called once. (even though the initial value is only used on the first render, the function which initializes it still gets called))
//https://stackoverflow.com/questions/58539813/lazy-initial-state-where-to-use-it
// and https://dmitripavlutin.com/react-usestate-hook-guide/#3-lazy-initialization-of-state
// Or, can use https://www.npmjs.com/package/@rehooks/local-storage which syncs between tabs
const [cart, setCart] = useState(() => {
try {
return JSON.parse(localStorage.getItem("cart")) ?? [];
} catch {
console.error(
"The localStorage cart could not be parsed into JSON. Resetting to an empty array."
);
return [];
}
});
const [cart, dispatch] = useReducer(
cartReducer,
JSON.parse(localStorage.getItem("cart")) ?? []
);

// Persist cart in localStorage
useEffect(() => localStorage.setItem("cart", JSON.stringify(cart)), [cart]);

function addToCart(id, sku) {
setCart((items) => {
const alreadyInCart = items.find((i) => i.sku === sku);
if (alreadyInCart) {
// Return new array with matching item replaced
return items.map((i) =>
i.sku === sku ? { ...i, quantity: i.quantity + 1 } : i
);
} else {
// Return new array with new item appended
return [...items, { id, sku, quantity: 1 }];
}
});
}

function updateQuantity(sku, quantity) {
setCart((items) => {
return quantity === 0
? items.filter((i) => i.sku !== sku)
: items.map((i) => (i.sku === sku ? { ...i, quantity } : i));
});
}

function emptyCart() {
setCart([]);
}

const numItemsInCart = cart.reduce((total, item) => total + item.quantity, 0);

return (
Expand All @@ -71,19 +33,19 @@ function App() {
element={
<Cart
cart={cart}
updateQuantity={updateQuantity}
dispatch={dispatch}
numItemsInCart={numItemsInCart}
/>
}
/>
<Route
path="/checkout"
element={<Checkout emptyCart={emptyCart} />}
element={<Checkout dispatch={dispatch} />}
/>
<Route path="/:category" element={<Products />} />
<Route
path="/:category/:id"
element={<Detail addToCart={addToCart} />}
element={<Detail dispatch={dispatch} />}
/>
<Route path="/page-not-found" />
</Routes>
Expand Down
12 changes: 10 additions & 2 deletions src/Cart.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { Link, useNavigate } from "react-router-dom";
import useFetchAll from "./services/useFetchAll";
import Spinner from "./Spinner";

export default function Cart({ cart, updateQuantity, numItemsInCart }) {
export default function Cart({ cart, dispatch, numItemsInCart }) {
// Using ref since not rendered, and need to avoid re-allocating on each render.
const uniqueIdsInCart = [...new Set(cart.map((i) => i.id))];
const urls = uniqueIdsInCart.map((id) => `products/${id}`);
const [products, loading, error] = useFetchAll(urls);
Expand All @@ -25,7 +26,14 @@ export default function Cart({ cart, updateQuantity, numItemsInCart }) {
<p>
<select
aria-label={`Select quantity for ${name} size ${size}`}
onChange={(e) => updateQuantity(sku, parseInt(e.target.value))}
onChange={(e) => {
dispatch({
type: "changeQuantity",
id,
sku,
quantity: parseInt(e.target.value),
});
}}
value={quantity}
>
<option value="0">Remove</option>
Expand Down
4 changes: 2 additions & 2 deletions src/Checkout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const STATUS = {
COMPLETED: "COMPLETED",
};

export default function Checkout({ emptyCart }) {
export default function Checkout({ dispatch }) {
const [address, setAddress] = useState(newAddress);
// Object with property for each field that has been touched.
const [touched, setTouched] = useState({});
Expand Down Expand Up @@ -52,7 +52,7 @@ export default function Checkout({ emptyCart }) {
setStatus(STATUS.SUBMITTING);
try {
await saveShippingAddress(address);
emptyCart();
dispatch({ type: "empty" });
setStatus(STATUS.COMPLETED);
} catch (err) {
setSaveError(err);
Expand Down
8 changes: 6 additions & 2 deletions src/Detail.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import useFetch from "./services/useFetch";
import Spinner from "./Spinner";
import PageNotFound from "./PageNotFound";

export default function Detail({ addToCart }) {
export default function Detail({ dispatch }) {
const navigate = useNavigate();
const [sku, setSku] = useState("");
const { id } = useParams();
Expand Down Expand Up @@ -37,7 +37,11 @@ export default function Detail({ addToCart }) {
className="btn btn-primary"
disabled={!sku}
onClick={() => {
addToCart(product.id, sku);
dispatch({
type: "add",
id,
sku,
});
navigate("/cart");
}}
>
Expand Down
30 changes: 30 additions & 0 deletions src/cartReducer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export default function cartReducer(cart, action) {
switch (action.type) {
case "add": {
const { id, sku } = action;
const itemInCart = cart.find((i) => i.sku === sku);
if (itemInCart) {
// Return new array with matching item replaced
return cart.map((i) =>
i.sku === sku ? { ...i, quantity: i.quantity + 1 } : i
);
} else {
// Return new array with new item appended
return [...cart, { id, sku, quantity: 1 }];
}
}

case "empty":
return [];

case "changeQuantity": {
const { quantity, sku } = action;
return quantity === 0
? cart.filter((i) => i.sku !== sku)
: cart.map((i) => (i.sku === sku ? { ...i, quantity } : i));
}

default:
throw new Error("Unhandled action" + action.type);
}
}