九、案例:汉堡到家

郁子大约 21 分钟约 6192 字笔记React18李立超

(一)项目准备

1. src/index.js

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";

// 设置移动端适配
// 100/x 表示 视口宽度为x rem
document.documentElement.style.fontSize = 100 / 750 + "vw";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
);

2. src/index.css

* {
  box-sizing: border-box;
}
body {
  margin: 0;
}

3. src/App.js

import React from "react";

export default function App() {
  return (
    <>
      <div style={{ width: "750rem", height: 200, backgroundColor: "#bfa" }}></div>
    </>
  );
}

(二)完成 Meals 组件

1. src/index.css

* {
  box-sizing: border-box;
}
body {
  margin: 0;
}
img {
  vertical-align: middle;
}

2. src/App.js

import React from "react";
import { Meals } from "./components/Meals/Meals";

export default function App() {
  return (
    <>
      {/* 测试移动端适配视口宽度 */}
      {/* <div
        style={{ width: "750rem", height: 200, backgroundColor: "#bfa" }}
      ></div> */}
      <Meals />
    </>
  );
}

3. src/components/Meals/Meals.jsx

import React from "react";
import { Meal } from "./Meal/Meal";
import classes from "./Meals.module.css";

export const Meals = () => {
  return (
    // 设置滚动条出现在Meals而不是body
    <div className={classes.Meals}>
      <Meal />
      <Meal />
      <Meal />
      <Meal />
      <Meal />
      <Meal />
      <Meal />
      <Meal />
      <Meal />
      <Meal />
      <Meal />
      <Meal />
    </div>
  );
};

4. src/components/Meals/Meals.module.css

.Meals {
  position: absolute;
  top: 0;
  bottom: 0;
  overflow: auto;
}

5. src/components/Meals/Meal/Meal.jsx

import React from "react";
import classes from "./Meal.module.css";

export const Meal = () => {
  return (
    <div className={classes.Meal}>
      <div className={classes.ImgBox}>
        <img src="/img/meals/1.png" />
      </div>
      <div>
        <h2 className={classes.Title}>汉堡包</h2>
        <p className={classes.Desc}>百分百纯牛肉搭配爽脆酸瓜洋葱粒与美味番茄酱经典滋味让你无法抵挡!</p>
        <div className={classes.PriceWrap}>
          <span className={classes.Price}>12</span>
          <div>数量</div>
        </div>
      </div>
    </div>
  );
};

5. src/components/Meals/Meal/Meal.module.css

.Meal {
  border-bottom: 1px #f2f2f2 solid;
  padding: 30rem 20rem;
  display: flex;
  align-items: center;
}
.ImgBox {
  width: 280rem;
}
img {
  width: 100%;
}
.Title {
  font-size: 18px;
  font-weight: normal;
  margin: 0;
}
.Desc {
  margin: 0;
  font-size: 12px;
  color: #bbb;
  padding-right: 40rem;
}
.PriceWrap {
  margin-top: 40rem;
  display: flex;
  justify-content: space-between;
}
.Price {
  font-weight: bold;
  font-size: 20px;
}
.Price::before {
  content: "¥";
  font-size: 12px;
}

(三)引入 FontAwesome

1.安装依赖

yarn add @fortawesome/fontawesome-svg-core
yarn add @fortawesome/free-solid-svg-icons
yarn add @fortawesome/free-regular-svg-icons
yarn add @fortawesome/react-fontawesome@latest

// 也可以一起安装
yarn add @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/free-regular-svg-icons @fortawesome/react-fontawesome@latest

2.引入组件

import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";

3.引入图标

import { faPlus } from "@fortawesome/free-solid-svg-icons";

4.使用组件

<FontAwesomeIcon icon="{faPlus}" />

5. src/components/UI/Counter/Counter.jsx

import React from "react";
import classes from "./Counter.module.css";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPlus, faMinus } from "@fortawesome/free-solid-svg-icons";

export const Counter = (props) => {
  return (
    <div className={classes.Counter}>
      {props.amount && props.amount !== 0 ? (
        <>
          <button className={classes.Sub}>
            <FontAwesomeIcon icon={faMinus} />
          </button>
          <span className={classes.count}>{props.amount}</span>
        </>
      ) : null}

      <button className={classes.Add}>
        <FontAwesomeIcon icon={faPlus} />
      </button>
    </div>
  );
};

6. src/components/UI/Counter/Counter.module.css

.Counter {
  display: flex;
  align-items: center;
}
.Sub,
.Add {
  border: none;
  background-color: #fcbf49;
  width: 36rem;
  height: 36rem;
  line-height: 36rem;
  border-radius: 50%;
  font-size: 14px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.Sub {
  background-color: #fff;
  border: 1px solid #000;
}
.count {
  font-size: 16px;
  margin: 0 5px;
}

7. src/components/Meals/Meal/Meal.jsx

import React from "react";
import { Counter } from "../../UI/Counter/Counter";
import classes from "./Meal.module.css";

export const Meal = () => {
  return (
    <div className={classes.Meal}>
      <div className={classes.ImgBox}>
        <img src="/img/meals/1.png" alt="汉堡包" />
      </div>
      <div>
        <h2 className={classes.Title}>汉堡包</h2>
        <p className={classes.Desc}>百分百纯牛肉搭配爽脆酸瓜洋葱粒与美味番茄酱经典滋味让你无法抵挡!</p>
        <div className={classes.PriceWrap}>
          <span className={classes.Price}>12</span>
          <div>
            <Counter amount={2} />
          </div>
        </div>
      </div>
    </div>
  );
};

8. src/components/Meals/Meal/Meal.module.css

.Meal {
  border-bottom: 1px #f2f2f2 solid;
  padding: 30rem 20rem;
  display: flex;
  align-items: center;
}
.ImgBox {
  width: 280rem;
}
img {
  width: 100%;
}
.Title {
  font-size: 18px;
  font-weight: normal;
  margin: 0;
}
.Desc {
  margin: 0;
  font-size: 12px;
  color: #bbb;
  padding-right: 40rem;
}
.PriceWrap {
  margin-top: 40rem;
  padding-right: 40rem;
  display: flex;
  justify-content: space-between;
}
.Price {
  font-weight: bold;
  font-size: 20px;
}
.Price::before {
  content: "¥";
  font-size: 12px;
}

(四)加载 Meals 数据

1. src/components/Meals/Meal/Meal.jsx

import React from "react";
import { Counter } from "../../UI/Counter/Counter";
import classes from "./Meal.module.css";

export const Meal = (props) => {
  return (
    <div className={classes.Meal}>
      <div className={classes.ImgBox}>
        <img src={props.img} alt={props.title} />
      </div>
      <div>
        <h2 className={classes.Title}>{props.title}</h2>
        <p className={classes.Desc}>{props.desc}</p>
        <div className={classes.PriceWrap}>
          <span className={classes.Price}>{props.price}</span>
          <div>
            <Counter amount={props.amount} />
          </div>
        </div>
      </div>
    </div>
  );
};

2. src/components/Meals/Meals.jsx

import React from "react";
import { Meal } from "./Meal/Meal";
import classes from "./Meals.module.css";

export const Meals = (props) => {
  return (
    // 设置滚动条出现在Meals而不是body
    <div className={classes.Meals}>
      {props.mealsData.map((meal) => (
        <Meal key={meal.id} {...meal} />
      ))}
    </div>
  );
};

(五)添加删除购物车中的商品

1. src/App.js

import React, { useState } from "react";
import { Meals } from "./components/Meals/Meals";

// 模拟一组食物数据
const MEALS_DATA = [
  {
    id: "1",
    title: "汉堡包",
    desc: "百分百纯牛肉配搭爽脆酸瓜洋葱粒与美味番茄酱经典滋味让你无法抵挡!",
    price: 12,
    img: "/img/meals/1.png",
  },
  {
    id: "2",
    title: "双层吉士汉堡",
    desc: "百分百纯牛肉与双层香软芝,加上松软面包及美味酱料,诱惑无人能挡!",
    price: 20,
    img: "/img/meals/2.png",
  },
  {
    id: "3",
    title: "巨无霸",
    desc: "两块百分百纯牛肉,搭配生菜、洋葱等新鲜食材,口感丰富,极致美味!",
    price: 24,
    img: "/img/meals/3.png",
  },
  {
    id: "4",
    title: "麦辣鸡腿汉堡",
    desc: "金黄脆辣的外皮,鲜嫩幼滑的鸡腿肉,多重滋味,一次打动您挑剔的味蕾!",
    price: 21,
    img: "/img/meals/4.png",
  },
  {
    id: "5",
    title: "板烧鸡腿堡",
    desc: "原块去骨鸡排嫩滑多汁,与翠绿新鲜的生菜和香浓烧鸡酱搭配,口感丰富!",
    price: 22,
    img: "/img/meals/5.png",
  },
  {
    id: "6",
    title: "麦香鸡",
    desc: "清脆爽口的生菜,金黄酥脆的鸡肉。营养配搭,好滋味的健康选择!",
    price: 14,
    img: "/img/meals/6.png",
  },
  {
    id: "7",
    title: "吉士汉堡包",
    desc: "百分百纯牛肉与香软芝士融为一体配合美味番茄醬丰富口感一咬即刻涌现!",
    price: 12,
    img: "/img/meals/7.png",
  },
];

export default function App() {
  const [mealsData, setMealsData] = useState(MEALS_DATA);

  const [cartData, setCartData] = useState({
    items: [],
    totalAmount: 0,
    totalPrice: 0,
  });

  // 加入购物车
  const addMealHandler = (meal) => {
    const newCart = { ...cartData };
    if (newCart.items.indexOf(meal) === -1) {
      newCart.items.push(meal);
      meal.amount = 1;
    } else {
      meal.amount += 1;
    }
    newCart.totalAmount += 1;
    newCart.totalPrice += meal.price;
    setCartData(newCart);
  };

  // 移出购物车
  const removeMealHandler = (meal) => {
    const newCart = { ...cartData };
    meal.amount -= 1;
    if (meal.amount === 0) newCart.items.filter((item) => item.id !== meal.id);
    newCart.totalAmount -= 1;
    newCart.totalPrice -= meal.price;
    setCartData(newCart);
  };

  return (
    <>
      <Meals mealsData={mealsData} onAddMeal={addMealHandler} onRemoveMeal={removeMealHandler} />
    </>
  );
}

2. src/components/Meals/Meals.jsx

import React from "react";
import { Meal } from "./Meal/Meal";
import classes from "./Meals.module.css";

export const Meals = (props) => {
  return (
    // 设置滚动条出现在Meals而不是body
    <div className={classes.Meals}>
      {props.mealsData.map((meal) => (
        <Meal key={meal.id} {...meal} meal={meal} onAddMeal={props.onAddMeal} onRemoveMeal={props.onRemoveMeal} />
      ))}
    </div>
  );
};

3. src/components/Meals/Meal/Meal.jsx

import React from "react";
import { Counter } from "../../UI/Counter/Counter";
import classes from "./Meal.module.css";

export const Meal = (props) => {
  return (
    <div className={classes.Meal}>
      <div className={classes.ImgBox}>
        <img src={props.img} alt={props.title} />
      </div>
      <div>
        <h2 className={classes.Title}>{props.title}</h2>
        <p className={classes.Desc}>{props.desc}</p>
        <div className={classes.PriceWrap}>
          <span className={classes.Price}>{props.price}</span>
          <div>
            <Counter {...props.meal} meal={props.meal} onAddMeal={props.onAddMeal} onRemoveMeal={props.onRemoveMeal} />
          </div>
        </div>
      </div>
    </div>
  );
};

(六)Context 的使用

1.XxxContext.Provider()

  • 表示数据的生产者,可以使用它来指定 Context 中的数据
  • 通过 value 指定,在该组件内的所有子组件都可以访问到该数据
  • 当通过 Context 访问数据时,会读取离引用该组件最近的 Providervalue
  • 如果没有 Provider ,则使用 Context 中定义的默认数据(一般不用,脱离 state

2.Context 使用方式一

  • 引入 Context
  • 使用 XxxContext.Consumer 组件来创建元素
    • 标签体需要一个回调函数,将 context 设置为回调函数的参数,即 context 中存储的数据

3.Context 使用方式二

  • 引入 Context
  • 使用钩子函数 useContext() 获取到 context
    • 需要一个 Context 作为参数

4.Context 相当于一个公共的存储空间

  • 将多个组件中都需要访问的数据统一存储到一个 Context 中,则无需 props 逐层传递
  • 通过 React.createContext() 创建 Context

5. src/App.js

import React from "react";
import { A } from "./components/A";
import { B } from "./components/B";
import TestContext from "./store/testContext";

export default function App() {
  return (
    <>
      <TestContext.Provider value={{ name: "bbb", age: 20 }}>
        <A />
        <TestContext.Provider value={{ name: "ccc", age: 22 }}>
          <B />
        </TestContext.Provider>
      </TestContext.Provider>
    </>
  );
}

6. src/components/A.jsx

import React from "react";
import TestContext from "../store/testContext";

export const A = () => {
  return (
    <TestContext.Consumer>
      {(ctx) => {
        return (
          <div>
            {ctx.name}--{ctx.age}
          </div>
        );
      }}
    </TestContext.Consumer>
  );
};

7. src/components/B.jsx

import React, { useContext } from "react";
import TestContext from "../store/testContext";

export const B = () => {
  const ctx = useContext(TestContext);

  return (
    <div>
      {ctx.name}--{ctx.age}
    </div>
  );
};

8. src/store/testContext.js

import React from "react";

const TestContext = React.createContext({
  name: "aaa",
  age: 19,
});

export default TestContext;

(七)完成搜索框

1. src/App.js

import React, { useState } from "react";
import { FilterMeals } from "./components/FilterMeals/FilterMeals";
import { Meals } from "./components/Meals/Meals";
import CartContext from "./store/CartContext";

// 模拟一组食物数据
const MEALS_DATA = [
  {
    id: "1",
    title: "汉堡包",
    desc: "百分百纯牛肉配搭爽脆酸瓜洋葱粒与美味番茄酱经典滋味让你无法抵挡!",
    price: 12,
    img: "/img/meals/1.png",
  },
  {
    id: "2",
    title: "双层吉士汉堡",
    desc: "百分百纯牛肉与双层香软芝,加上松软面包及美味酱料,诱惑无人能挡!",
    price: 20,
    img: "/img/meals/2.png",
  },
  {
    id: "3",
    title: "巨无霸",
    desc: "两块百分百纯牛肉,搭配生菜、洋葱等新鲜食材,口感丰富,极致美味!",
    price: 24,
    img: "/img/meals/3.png",
  },
  {
    id: "4",
    title: "麦辣鸡腿汉堡",
    desc: "金黄脆辣的外皮,鲜嫩幼滑的鸡腿肉,多重滋味,一次打动您挑剔的味蕾!",
    price: 21,
    img: "/img/meals/4.png",
  },
  {
    id: "5",
    title: "板烧鸡腿堡",
    desc: "原块去骨鸡排嫩滑多汁,与翠绿新鲜的生菜和香浓烧鸡酱搭配,口感丰富!",
    price: 22,
    img: "/img/meals/5.png",
  },
  {
    id: "6",
    title: "麦香鸡",
    desc: "清脆爽口的生菜,金黄酥脆的鸡肉。营养配搭,好滋味的健康选择!",
    price: 14,
    img: "/img/meals/6.png",
  },
  {
    id: "7",
    title: "吉士汉堡包",
    desc: "百分百纯牛肉与香软芝士融为一体配合美味番茄醬丰富口感一咬即刻涌现!",
    price: 12,
    img: "/img/meals/7.png",
  },
];

export default function App() {
  const [mealsData, setMealsData] = useState(MEALS_DATA);

  const [cartData, setCartData] = useState({
    items: [],
    totalAmount: 0,
    totalPrice: 0,
  });

  // 加入购物车
  const addItem = (meal) => {
    const newCart = { ...cartData };
    if (newCart.items.indexOf(meal) === -1) {
      newCart.items.push(meal);
      meal.amount = 1;
    } else {
      meal.amount += 1;
    }
    newCart.totalAmount += 1;
    newCart.totalPrice += meal.price;
    setCartData(newCart);
  };

  // 移出购物车
  const removeItem = (meal) => {
    const newCart = { ...cartData };
    meal.amount -= 1;
    if (meal.amount === 0) newCart.items.filter((item) => item.id !== meal.id);
    newCart.totalAmount -= 1;
    newCart.totalPrice -= meal.price;
    setCartData(newCart);
  };

  // 搜索meals
  const filterMeals = (keyWord) => {
    const newMealsData = MEALS_DATA.filter((item) => item.title.indexOf(keyWord) !== -1);
    setMealsData(newMealsData);
  };

  return (
    <>
      <CartContext.Provider value={{ ...cartData, addItem, removeItem }}>
        <FilterMeals onFilter={filterMeals} />
        <Meals mealsData={mealsData} />
      </CartContext.Provider>
    </>
  );
}

2. src/components/FilterMeals/FilterMeals.jsx

import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSearch } from "@fortawesome/free-solid-svg-icons";
import classes from "./FilterMeals.module.css";

export const FilterMeals = (props) => {
  const inputChangeHandler = (e) => {
    props.onFilter(e.target.value);
  };

  return (
    <div className={classes.FilterMeals}>
      <div className={classes.InputOrder}>
        <input type="text" placeholder="请输入关键字" className={classes.SearchInput} onChange={inputChangeHandler} />
        <FontAwesomeIcon icon={faSearch} className={classes.SearchIcon} />
      </div>
    </div>
  );
};

3. src/components/FilterMeals/FilterMeals.module.css

.FilterMeals {
  height: 120rem;
  position: fixed;
  left: 0;
  right: 0;
  z-index: 999;
  border-bottom: 1px solid #f2f2f2;
  display: flex;
  align-items: center;
  justify-content: center;
}

.InputOrder {
  position: relative;
  display: flex;
  align-items: center;
}

.SearchInput {
  width: 650rem;
  height: 70rem;
  border: none;
  font-size: 34rem;
  border-radius: 16px;
  padding-left: 60rem;
  outline: none;
  background-color: #f2f2f2;
}

.SearchIcon {
  color: #aaa;
  font-size: 40rem;
  position: absolute;
  left: 10rem;
}

4. src/store/CartContext.js

import React from "react";

const CartContext = React.createContext({
  items: [],
  totalAmount: 0,
  totalPrice: 0,
  addItem: () => {},
  removeItem: () => {},
});

export default CartContext;

(八)完成购物车条

1. src/App.js

import React, { useState } from "react";
import { Cart } from "./components/Cart/Cart";
import { FilterMeals } from "./components/FilterMeals/FilterMeals";
import { Meals } from "./components/Meals/Meals";
import CartContext from "./store/CartContext";

// 模拟一组食物数据
const MEALS_DATA = [
  {
    id: "1",
    title: "汉堡包",
    desc: "百分百纯牛肉配搭爽脆酸瓜洋葱粒与美味番茄酱经典滋味让你无法抵挡!",
    price: 12,
    img: "/img/meals/1.png",
  },
  {
    id: "2",
    title: "双层吉士汉堡",
    desc: "百分百纯牛肉与双层香软芝,加上松软面包及美味酱料,诱惑无人能挡!",
    price: 20,
    img: "/img/meals/2.png",
  },
  {
    id: "3",
    title: "巨无霸",
    desc: "两块百分百纯牛肉,搭配生菜、洋葱等新鲜食材,口感丰富,极致美味!",
    price: 24,
    img: "/img/meals/3.png",
  },
  {
    id: "4",
    title: "麦辣鸡腿汉堡",
    desc: "金黄脆辣的外皮,鲜嫩幼滑的鸡腿肉,多重滋味,一次打动您挑剔的味蕾!",
    price: 21,
    img: "/img/meals/4.png",
  },
  {
    id: "5",
    title: "板烧鸡腿堡",
    desc: "原块去骨鸡排嫩滑多汁,与翠绿新鲜的生菜和香浓烧鸡酱搭配,口感丰富!",
    price: 22,
    img: "/img/meals/5.png",
  },
  {
    id: "6",
    title: "麦香鸡",
    desc: "清脆爽口的生菜,金黄酥脆的鸡肉。营养配搭,好滋味的健康选择!",
    price: 14,
    img: "/img/meals/6.png",
  },
  {
    id: "7",
    title: "吉士汉堡包",
    desc: "百分百纯牛肉与香软芝士融为一体配合美味番茄醬丰富口感一咬即刻涌现!",
    price: 12,
    img: "/img/meals/7.png",
  },
];

export default function App() {
  const [mealsData, setMealsData] = useState(MEALS_DATA);

  const [cartData, setCartData] = useState({
    items: [],
    totalAmount: 0,
    totalPrice: 0,
  });

  // 加入购物车
  const addItem = (meal) => {
    const newCart = { ...cartData };
    if (newCart.items.indexOf(meal) === -1) {
      newCart.items.push(meal);
      meal.amount = 1;
    } else {
      meal.amount += 1;
    }
    newCart.totalAmount += 1;
    newCart.totalPrice += meal.price;
    setCartData(newCart);
  };

  // 移出购物车
  const removeItem = (meal) => {
    const newCart = { ...cartData };
    meal.amount -= 1;
    if (meal.amount === 0) newCart.items.filter((item) => item.id !== meal.id);
    newCart.totalAmount -= 1;
    newCart.totalPrice -= meal.price;
    setCartData(newCart);
  };

  // 搜索meals
  const filterMeals = (keyWord) => {
    const newMealsData = MEALS_DATA.filter((item) => item.title.indexOf(keyWord) !== -1);
    setMealsData(newMealsData);
  };

  return (
    <>
      <CartContext.Provider value={{ ...cartData, addItem, removeItem }}>
        <FilterMeals onFilter={filterMeals} />
        <Meals mealsData={mealsData} />
        <Cart />
      </CartContext.Provider>
    </>
  );
}

2. src/components/Cart/Cart.jsx

import React, { useContext } from "react";
import classes from "./Cart.module.css";
import iconImg from "../../asset/bag.png";
import CartContext from "../../store/CartContext";

export const Cart = () => {
  const ctx = useContext(CartContext);

  return (
    <div className={classes.Cart}>
      <div className={classes.Icon}>
        <img src={iconImg} alt="包" />
        {ctx.totalAmount === 0 ? null : <span className={classes.TotalAmount}>{ctx.totalAmount}</span>}
      </div>

      {ctx.totalAmount === 0 ? <p className={classes.NoMeal}>未选购商品</p> : <p className={classes.Price}>{ctx.totalPrice}</p>}

      <button className={`${classes.Button} ${ctx.totalAmount === 0 ? classes.Disabled : ""}`}>去结算</button>
    </div>
  );
};

3. src/components/Cart/Cart.module.css

.Cart {
  position: fixed;
  bottom: 30rem;
  left: 0;
  right: 0;
  margin: auto;
  width: 700rem;
  height: 80rem;
  background-color: #333;
  border-radius: 20px;
  display: flex;
  justify-content: space-between;
  z-index: 9999;
}
.Icon {
  width: 80rem;
  position: absolute;
  bottom: -6rem;
}
.Icon img {
  width: 100%;
}
.TotalAmount {
  position: absolute;
  right: -10rem;
  width: 36rem;
  height: 36rem;
  line-height: 36rem;
  text-align: center;
  background-color: #f00;
  border-radius: 50%;
  color: #fff;
  font-weight: bold;
  font-size: 22rem;
}
.Price,
.NoMeal {
  color: #fff;
  margin-left: 120rem;
  display: flex;
  align-items: center;
  font-weight: bold;
  font-size: 36rem;
}
.Price::before {
  content: "¥";
  font-size: 24rem;
}
.NoMeal {
  color: #aaa;
}
.Button {
  border: none;
  background-color: #fcbf49;
  width: 200rem;
  border-radius: 20px;
  font-size: 36rem;
}
.Disabled {
  background-color: #777;
  color: #ccc;
}

(九)完成购物车详情

1. src/components/Cart/Cart.jsx

import React, { useContext, useState } from "react";
import classes from "./Cart.module.css";
import iconImg from "../../asset/bag.png";
import CartContext from "../../store/CartContext";
import { CartDetails } from "./CartDetails/CartDetails";

export const Cart = () => {
  const ctx = useContext(CartContext);
  const [showDetails, setShowDetails] = useState(false);

  const toggleDetailsHandler = () => {
    if (ctx.totalAmount === 0) return;
    setShowDetails((prevState) => !prevState);
  };

  return (
    <div className={classes.Cart} onClick={toggleDetailsHandler}>
      {showDetails && <CartDetails />}

      <div className={classes.Icon}>
        <img src={iconImg} alt="包" />
        {ctx.totalAmount === 0 ? null : <span className={classes.TotalAmount}>{ctx.totalAmount}</span>}
      </div>

      {ctx.totalAmount === 0 ? <p className={classes.NoMeal}>未选购商品</p> : <p className={classes.Price}>{ctx.totalPrice}</p>}

      <button className={`${classes.Button} ${ctx.totalAmount === 0 ? classes.Disabled : ""}`}>去结算</button>
    </div>
  );
};

2. src/components/Cart/CartDetails/CartDetails.jsx

import React, { useContext } from "react";
import { BackDrop } from "../../UI/BackDrop/BackDrop";
import { Meal } from "../../Meals/Meal/Meal";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTrash } from "@fortawesome/free-solid-svg-icons";
import CartContext from "../../../store/CartContext";
import classes from "./CartDetails.module.css";

export const CartDetails = () => {
  const ctx = useContext(CartContext);

  return (
    <div>
      <BackDrop>
        <div className={classes.CartDetails} onClick={(e) => e.stopPropagation()}>
          <header className={classes.Header}>
            <h2 className={classes.Title}>餐品详情</h2>
            <div className={classes.Clear}>
              <FontAwesomeIcon icon={faTrash} />
              <span>清空购物车</span>
            </div>
          </header>

          <div className={classes.MealList}>
            {ctx.items.map((meal) => {
              return <Meal key={meal.id} {...meal} meal={meal} noDesc />;
            })}
          </div>
        </div>
      </BackDrop>
    </div>
  );
};

3. src/components/Cart/CartDetails/CartDetails.module.css

.CartDetails {
  background-color: #fff;
  width: 750rem;
  max-height: 1200rem;
  position: absolute;
  bottom: 0;
  padding-bottom: 120rem;
  border-top-left-radius: 20px;
  border-top-right-radius: 20px;
  display: flex;
  flex-direction: column;
}
.Header {
  display: flex;
  justify-content: space-between;
  padding: 30rem;
}
.Title {
  font-weight: bold;
  font-size: 28rem;
  margin: 0;
}
.Clear {
  color: #aaa;
  font-size: 24rem;
}
.Clear span {
  margin-left: 12rem;
}
.MealList {
  overflow: auto;
}

4. src/components/UI/BackDrop/BackDrop.jsx

import React from "react";
import ReactDOM from "react-dom";
import classes from "./BackDrop.module.css";

const backDropRoot = document.getElementById("backdrop");

export const BackDrop = (props) => {
  return ReactDOM.createPortal(<div className={`${classes.BackDrop} ${props.className}`}>{props.children}</div>, backDropRoot);
};

5. src/components/UI/BackDrop/BackDrop.module.css

.BackDrop {
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  margin: auto;
  background-color: rgba(0, 0, 0, 0.3);
  z-index: 999;
}

(十)完成购物车列表和支付

1. src/components/Cart/Cart.jsx

import React, { useContext, useState } from "react";
import classes from "./Cart.module.css";
import iconImg from "../../asset/bag.png";
import CartContext from "../../store/CartContext";
import { CartDetails } from "./CartDetails/CartDetails";
import { CheckOut } from "./CheckOut/CheckOut";

export const Cart = () => {
  const ctx = useContext(CartContext);
  const [showDetails, setShowDetails] = useState(false);
  const [showCheckout, setShowCheckout] = useState(false);

  const toggleDetailsHandler = () => {
    if (ctx.totalAmount === 0) {
      setShowDetails(false);
      return;
    }
    setShowDetails((prevState) => !prevState);
  };

  const showCheckoutHandler = () => {
    if (ctx.totalAmount === 0) return;
    setShowCheckout(true);
  };

  const hideCheckoutHandler = () => {
    setShowCheckout(false);
  };

  return (
    <div className={classes.Cart} onClick={toggleDetailsHandler}>
      {showCheckout && <CheckOut onHide={hideCheckoutHandler} />}

      {showDetails && <CartDetails />}

      <div className={classes.Icon}>
        <img src={iconImg} alt="包" />
        {ctx.totalAmount === 0 ? null : <span className={classes.TotalAmount}>{ctx.totalAmount}</span>}
      </div>

      {ctx.totalAmount === 0 ? <p className={classes.NoMeal}>未选购商品</p> : <p className={classes.Price}>{ctx.totalPrice}</p>}

      <button className={`${classes.Button} ${ctx.totalAmount === 0 ? classes.Disabled : ""}`} onClick={showCheckoutHandler}>
        去结算
      </button>
    </div>
  );
};

2. src/components/Cart/CartDetails/CartDetails.jsx

import React, { useContext, useState } from "react";
import { BackDrop } from "../../UI/BackDrop/BackDrop";
import { Meal } from "../../Meals/Meal/Meal";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTrash } from "@fortawesome/free-solid-svg-icons";
import CartContext from "../../../store/CartContext";
import classes from "./CartDetails.module.css";
import { Confirm } from "../../UI/Confirm/Confirm";

export const CartDetails = () => {
  const ctx = useContext(CartContext);

  const [showConfirm, setShowConfirm] = useState(false);

  const showConfirmHandler = () => {
    setShowConfirm(true);
  };

  const cancelHandler = (e) => {
    e.stopPropagation();
    setShowConfirm(false);
  };
  const okHandler = () => {
    ctx.clearCart();
    setShowConfirm(false);
  };

  return (
    <div>
      <BackDrop>
        {showConfirm && <Confirm confirmText="确定清空购物车吗?" onCancel={cancelHandler} onOk={okHandler} />}

        <div className={classes.CartDetails} onClick={(e) => e.stopPropagation()}>
          <header className={classes.Header}>
            <h2 className={classes.Title}>餐品详情</h2>
            <div className={classes.Clear} onClick={showConfirmHandler}>
              <FontAwesomeIcon icon={faTrash} />
              <span>清空购物车</span>
            </div>
          </header>

          <div className={classes.MealList}>
            {ctx.items.map((meal) => {
              return <Meal key={meal.id} {...meal} meal={meal} noDesc />;
            })}
          </div>
        </div>
      </BackDrop>
    </div>
  );
};

3. src/components/Cart/CheckOut/CheckOut.jsx

import React, { useContext } from "react";
import ReactDOM from "react-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import classes from "./CheckOut.module.css";
import CartContext from "../../../store/CartContext";
import { CheckOutItem } from "./CheckOutItem/CheckOutItem";
import { Bar } from "./Bar/Bar";

const checkOutRoot = document.getElementById("checkout");

export const CheckOut = (props) => {
  const ctx = useContext(CartContext);

  return ReactDOM.createPortal(
    <div className={classes.CheckOut}>
      <div className={classes.Close}>
        <FontAwesomeIcon icon={faXmark} onClick={() => props.onHide()} />
      </div>

      <div className={classes.MealsDesc}>
        <header className={classes.Header}>
          <h2 className={classes.Title}>餐品详情</h2>
        </header>

        <div className={classes.Meals}>
          {ctx.items.map((meal) => {
            return <CheckOutItem key={meal.id} {...meal} meal={meal} />;
          })}
        </div>

        <footer className={classes.Footer}>
          <p className={classes.TotalPrice}>{ctx.totalPrice}</p>
        </footer>
      </div>

      <Bar totalPrice={ctx.totalPrice} />
    </div>,
    checkOutRoot,
  );
};

4. src/components/Cart/CheckOut/CheckOut.module.css

.CheckOut {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  z-index: 9999;
  background-color: #eee;
  padding: 20rem;
}
.Close {
  color: #000;
  font-size: 36rem;
  font-weight: bold;
}
.MealsDesc {
  background-color: #fff;
  border-radius: 20px;
  padding: 0 20rem;
  position: relative;
}
.Header {
  border-bottom: 1px solid #f2f2f2;
  display: flex;
  align-items: center;
  height: 80rem;
  font-size: 20rem;
}
.Meals {
  max-height: 900rem;
  overflow: auto;
}
.Footer {
  height: 120rem;
  border-top: 1px dashed #f2f2f2;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  font-weight: bold;
  font-size: 30rem;
}
.Footer::before,
.Footer::after {
  content: "";
  position: absolute;
  width: 20rem;
  height: 20rem;
  background-color: #eee;
  border-radius: 50%;
  bottom: 110rem;
  left: -10rem;
}
.Footer::after {
  left: auto;
  right: -10rem;
}
.TotalPrice::before {
  content: "合计:¥";
  font-weight: normal;
  font-size: 18rem;
  margin-right: 10rem;
}

5. src/components/Cart/CheckOut/CheckOutItem/CheckOutItem.jsx

import React from "react";
import { Counter } from "../../../UI/Counter/Counter";
import classes from "./CheckOutItem.module.css";

export const CheckOutItem = (props) => {
  return (
    <div className={classes.CheckOutItem}>
      <div className={classes.MealImg}>
        <img src={props.img} alt={props.title} />
      </div>
      <div className={classes.Desc}>
        <h2 className={classes.Title}>{props.title}</h2>
        <div className={classes.PriceOuter}>
          <Counter {...props.meal} meal={props.meal} />
          <div className={classes.Price}>{props.price * props.amount}</div>
        </div>
      </div>
    </div>
  );
};

6. src/components/Cart/CheckOut/CheckOutItem/CheckOutItem.module.css

.CheckOutItem {
  display: flex;
}
.MealImg {
  width: 140rem;
}
.MealImg img {
  width: 100%;
}
.Desc {
  flex: auto;
  margin-left: 20rem;
  display: flex;
  flex-direction: column;
  justify-content: space-evenly;
}
.Title {
  font-size: 24rem;
}
.PriceOuter {
  display: flex;
  justify-content: space-between;
}
.Price {
  font-size: 26rem;
  font-weight: bold;
}
.Price::before {
  content: "¥";
  font-weight: normal;
  font-size: 24rem;
}

7. src/components/Cart/CheckOut/Bar/Bar.jsx

import React from "react";
import classes from "./Bar.module.css";

export const Bar = (props) => {
  return (
    <div className={classes.Bar}>
      <div className={classes.TotalPrice}>{props.totalPrice}</div>
      <button className={classes.Button}>去支付</button>
    </div>
  );
};

8. src/components/Cart/CheckOut/Bar/Bar.module.css

.Bar {
  position: fixed;
  bottom: 30rem;
  left: 0;
  right: 0;
  margin: auto;
  width: 700rem;
  height: 80rem;
  background-color: #333;
  border-radius: 20px;
  display: flex;
  justify-content: space-between;
  z-index: 9999;
}
.Button {
  border: none;
  background-color: #fcbf49;
  width: 200rem;
  border-radius: 20px;
  font-size: 36rem;
}
.TotalPrice,
.NoMeal {
  color: #fff;
  flex: 1;
  margin-left: 50rem;
  display: flex;
  align-items: center;
  font-weight: bold;
  font-size: 36rem;
}
.TotalPrice::before {
  content: "¥";
  font-size: 24rem;
}

9. src/components/UI/Confirm/Confirm.jsx

import React from "react";
import { BackDrop } from "../BackDrop/BackDrop";
import classes from "./Confirm.module.css";

export const Confirm = (props) => {
  return (
    <BackDrop className={classes.ConfirmOuter} onClick={(e) => props.onCancel(e)}>
      <div className={classes.Confirm}>
        <p className={classes.ConfirmText}>{props.confirmText}</p>
        <div>
          <button className={classes.Cancel} onClick={(e) => props.onCancel(e)}>
            取消
          </button>
          <button className={classes.Ok} onClick={() => props.onOk()}>
            确认
          </button>
        </div>
      </div>
    </BackDrop>
  );
};

10. src/components/UI/Confirm/Confirm.module.css

.ConfirmOuter {
  z-index: 9999;
}
.Confirm {
  width: 700rem;
  height: 300rem;
  background-color: #fff;
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  margin: auto;
  border-radius: 20px;
  display: flex;
  flex-direction: column;
  justify-content: space-evenly;
  align-items: center;
}
.ConfirmText {
  font-size: 38rem;
}
.Cancel,
.Ok {
  border: 1px solid #ccc;
  background-color: #fff;
  width: 180rem;
  height: 60rem;
  border-radius: 20px;
  font-size: 28rem;
}
.Ok {
  background-color: #fcbf49;
  border: none;
  margin-left: 50rem;
}

11. src/store/CartContext.js

import React from "react";

const CartContext = React.createContext({
  items: [],
  totalAmount: 0,
  totalPrice: 0,
  addItem: () => {},
  removeItem: () => {},
  clearCart: () => {},
});

export default CartContext;

(十一)setState 的执行流程

  • 直接在函数体中调用 setState 时,会触发 Too many re-renders. 错误
  • 一般情况下,当 state 新旧值相同时, setState 不触发组件重新渲染

1.setState() 执行流程(函数组件)

  • setCount() => dispatchSetDate() => 先判断组件当前处于什么阶段 =>

1)渲染阶段

  • 不会检查 state 值是否相同

2)非渲染阶段

  • 检查 state 值是否相同
    • 值不同:对组件重新渲染
    • 值相同:不对组件重新渲染, React 会在某些情况下继续执行当前组件渲染,不会触发子组件渲染,也不会产生实际的效果【通常发生在值第一次相同时】

2. src/App.jsx

import React, { useState } from "react";
import { B } from "./B";

export const App = () => {
  console.log("App重新渲染了");

  const [count, setCount] = useState(0);

  // setCount(0);

  /*
    count 0
    点击1 count 1 App重新渲染
    点击2 count 1 App重新渲染
    点击3 count 1 
  */
  const clickHandler = () => {
    console.log("点击按钮");
    setCount(1);
  };

  return (
    <div>
      {count}
      <B />
      <button onClick={clickHandler}>点我一下</button>
    </div>
  );
};

3. src/index.js

import ReactDOM from "react-dom/client";
import { App } from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));

root.render(<App />);

4. src/B.jsx

import React from "react";

export const B = () => {
  console.log("B组件重新渲染了");
  return <div>B</div>;
};

(十二)useEffect

  • useEffect() 是一个钩子函数
  • 需要一个函数作为参数,该函数将在组件渲染完毕后执行
  • 可以将会产生副作用的代码编写到该回调函数中

1. src/App.jsx

import React, { useEffect, useState } from "react";
import { B } from "./B";

export const App = () => {
  console.log("App重新渲染了");
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount(1);
  });

  const clickHandler = () => {
    console.log("点击按钮");
    setCount(1);
  };

  return (
    <div>
      {count}
      <B />
      <button onClick={clickHandler}>点我一下</button>
    </div>
  );
};

2. src/index.js

import ReactDOM from "react-dom/client";
import { App } from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));

root.render(<App />);

3. src/B.jsx

import React from "react";

export const B = () => {
  console.log("B组件重新渲染了");
  return <div>B</div>;
};

(十三)Effect 的依赖项和清理函数

  • Effect 的回调函数中,可以指定一个函数作为返回值
  • 该函数称为清理函数,会在下次 Effect 执行前调用

1.useEffect() 是一个钩子函数

  • 需要一个函数作为参数,该函数将在组件渲染完毕后执行(每次渲染完成都会调用
    • 可以将会产生副作用的代码编写到该回调函数中
  • 可以传递第二个参数,是一个数组
    • 在数组中指定 Effect 的依赖项,只有依赖项发生变化时才会执行第一个参数的回调函数
  • 通常会将 Effect 中使用的所有变量都设置为依赖项
  • setState() 是由钩子函数 useState() 生成的
    • useState() 会确保组件的每次渲染都会获取到相同的 setState() 对象,所以 setState() 可以不设置到依赖中
  • 如果依赖项设置为空数组,则 Effect 只在组件初始化时执行一次

2. src/components/FilterMeals/FilterMeals.jsx

import React, { useEffect, useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSearch } from "@fortawesome/free-solid-svg-icons";
import classes from "./FilterMeals.module.css";

export const FilterMeals = (props) => {
  const [keyWord, setKeyWord] = useState("");

  const inputChangeHandler = (e) => {
    setKeyWord(e.target.value.trim());
    // props.onFilter(e.target.value);
  };

  useEffect(() => {
    // 降低数据过滤的次数,输完了再过滤
    // 当用户停止输入动作1s后,才做查询
    // 开启定时器的同时应该关闭上一个定时器
    const timer = setTimeout(() => {
      props.onFilter(keyWord);
    }, 1000);

    return () => {
      clearTimeout(timer);
    };
  }, [props, keyWord]);

  return (
    <div className={classes.FilterMeals}>
      <div className={classes.InputOrder}>
        <input type="text" placeholder="请输入关键字" value={keyWord} className={classes.SearchInput} onChange={inputChangeHandler} />
        <FontAwesomeIcon icon={faSearch} className={classes.SearchIcon} />
      </div>
    </div>
  );
};

3. src/components/Cart/Cart.jsx

import React, { useContext, useEffect, useState } from "react";
import classes from "./Cart.module.css";
import iconImg from "../../asset/bag.png";
import CartContext from "../../store/CartContext";
import { CartDetails } from "./CartDetails/CartDetails";
import { CheckOut } from "./CheckOut/CheckOut";

export const Cart = () => {
  const ctx = useContext(CartContext);
  const [showDetails, setShowDetails] = useState(false);
  const [showCheckout, setShowCheckout] = useState(false);

  // 在组件每次重新渲染时,检查一下商品的总数量,如果为0则隐藏弹出层
  // 组件每次重新渲染时,组件的函数体就会执行
  // 以下代码会报错(Too many re-renders.)
  // if (ctx.totalAmount === 0) setShowDetails(false);
  useEffect(() => {
    console.log("useEffect执行了");
    if (ctx.totalAmount === 0) {
      setShowDetails(false);
      setShowCheckout(false);
    }
  }, [ctx, setShowDetails, setShowCheckout]);
  // useEffect(() => {
  //   console.log("useEffect执行了");
  //   if (ctx.totalAmount === 0) {
  //     setShowDetails(false);
  //     setShowCheckout(false);
  //   }
  // }, []);

  const toggleDetailsHandler = () => {
    if (ctx.totalAmount === 0) {
      setShowDetails(false);
      return;
    }
    setShowDetails((prevState) => !prevState);
  };

  const showCheckoutHandler = () => {
    if (ctx.totalAmount === 0) return;
    setShowCheckout(true);
  };

  const hideCheckoutHandler = () => {
    setShowCheckout(false);
  };

  return (
    <div className={classes.Cart} onClick={toggleDetailsHandler}>
      {showCheckout && <CheckOut onHide={hideCheckoutHandler} />}

      {showDetails && <CartDetails />}

      <div className={classes.Icon}>
        <img src={iconImg} alt="包" />
        {ctx.totalAmount === 0 ? null : <span className={classes.TotalAmount}>{ctx.totalAmount}</span>}
      </div>

      {ctx.totalAmount === 0 ? <p className={classes.NoMeal}>未选购商品</p> : <p className={classes.Price}>{ctx.totalPrice}</p>}

      <button className={`${classes.Button} ${ctx.totalAmount === 0 ? classes.Disabled : ""}`} onClick={showCheckoutHandler}>
        去结算
      </button>
    </div>
  );
};

(十四)useReducer

  • 整合器(功能聚合): useReducer(reducer, initialArg, init)

1.参数

  • reducer :整合函数
    • 对于当前 state 的所有操作都应该在该函数中定义
    • 该函数的返回值会成为 state 的新值
    • reducer 在执行时会收到两个参数
      • state :当前最新的 state 值
      • action :需要一个对象,存储 dispatch 所发送的指令,可以根据 action 中不用的 type 返回不同的值 => 执行不同的操作
  • initialArgstate 的初始值,作用和 useState() 中的值是一样的

2.返回值

  • 数组
  • 参数 1: state
    • 用来获取 state 的值
  • 参数 2: stateDispatch
    • 用来修改 state 的派发器
    • 通过派发器可以发送操作 state 的命令,具体的修改行为将由 reducer 函数执行

注意

为了避免 reducer 在每次组件渲染时会重复创建

通常 reducer 会定义到组件的外部再引入

3. src/App.jsx

import React, { useReducer, useState } from "react";
import countReducer from "./reducers/countReducer";

export const App = () => {
  // const [count, setCount] = useState(1);
  // const addHandler = () => {
  //   setCount((pre) => pre + 1);
  // };
  // const subHandler = () => {
  //   setCount((pre) => pre - 1);
  // };

  // const [count, countDispatch] = useReducer((state, action) => {
  //   // console.log("reducer执行了", state, action);
  //   // return state + 1;
  //   switch (action.type) {
  //     case "ADD":
  //       return state + 1;
  //     case "SUB":
  //       return state - 1;
  //     default:
  //       return state;
  //   }
  // }, 1);
  const [count, countDispatch] = useReducer(countReducer, 1);
  const addHandler = () => {
    countDispatch({ type: "ADD" });
  };
  const subHandler = () => {
    countDispatch({ type: "SUB" });
  };

  return (
    <div
      style={{
        fontSize: 30,
        width: 200,
        height: 200,
        backgroundColor: "lightblue",
        margin: "100px auto",
        textAlign: "center",
      }}
    >
      {/* <button onClick={subHandler}>减少</button>
      {count}
      <button onClick={addHandler}>增加</button> */}

      <button onClick={subHandler}>减少</button>
      {count}
      <button onClick={addHandler}>增加</button>
    </div>
  );
};

4. src/reducers/countReducer.js

export default (state, action) => {
  // console.log("reducer执行了", state, action);
  // return state + 1;
  switch (action.type) {
    case "ADD":
      return state + 1;
    case "SUB":
      return state - 1;
    default:
      return state;
  }
};

(十五)使用 useReducer 修改案例

1. src/App.js

import React, { useState, useReducer } from "react";
import { Cart } from "./components/Cart/Cart";
import { FilterMeals } from "./components/FilterMeals/FilterMeals";
import { Meals } from "./components/Meals/Meals";
import CartContext from "./store/CartContext";
import cartReducer from "./reducers/cartReducer";

// 模拟一组食物数据
const MEALS_DATA = [
  {
    id: "1",
    title: "汉堡包",
    desc: "百分百纯牛肉配搭爽脆酸瓜洋葱粒与美味番茄酱经典滋味让你无法抵挡!",
    price: 12,
    img: "/img/meals/1.png",
  },
  {
    id: "2",
    title: "双层吉士汉堡",
    desc: "百分百纯牛肉与双层香软芝,加上松软面包及美味酱料,诱惑无人能挡!",
    price: 20,
    img: "/img/meals/2.png",
  },
  {
    id: "3",
    title: "巨无霸",
    desc: "两块百分百纯牛肉,搭配生菜、洋葱等新鲜食材,口感丰富,极致美味!",
    price: 24,
    img: "/img/meals/3.png",
  },
  {
    id: "4",
    title: "麦辣鸡腿汉堡",
    desc: "金黄脆辣的外皮,鲜嫩幼滑的鸡腿肉,多重滋味,一次打动您挑剔的味蕾!",
    price: 21,
    img: "/img/meals/4.png",
  },
  {
    id: "5",
    title: "板烧鸡腿堡",
    desc: "原块去骨鸡排嫩滑多汁,与翠绿新鲜的生菜和香浓烧鸡酱搭配,口感丰富!",
    price: 22,
    img: "/img/meals/5.png",
  },
  {
    id: "6",
    title: "麦香鸡",
    desc: "清脆爽口的生菜,金黄酥脆的鸡肉。营养配搭,好滋味的健康选择!",
    price: 14,
    img: "/img/meals/6.png",
  },
  {
    id: "7",
    title: "吉士汉堡包",
    desc: "百分百纯牛肉与香软芝士融为一体配合美味番茄醬丰富口感一咬即刻涌现!",
    price: 12,
    img: "/img/meals/7.png",
  },
];

export default function App() {
  const [mealsData, setMealsData] = useState(MEALS_DATA);

  const [cartData, cartDispatch] = useReducer(cartReducer, {
    items: [],
    totalAmount: 0,
    totalPrice: 0,
  });

  // 搜索meals
  const filterMeals = (keyWord) => {
    const newMealsData = MEALS_DATA.filter((item) => item.title.indexOf(keyWord) !== -1);
    setMealsData(newMealsData);
  };

  return (
    <>
      <CartContext.Provider value={{ ...cartData, cartDispatch }}>
        <FilterMeals onFilter={filterMeals} />
        <Meals mealsData={mealsData} />
        <Cart />
      </CartContext.Provider>
    </>
  );
}

2. src/store/CartContext.js

import React from "react";

const CartContext = React.createContext({
  items: [],
  totalAmount: 0,
  totalPrice: 0,
  cartDispatch: () => {},
});

export default CartContext;

3. src/reducers/cartReducer.js

const cartReducer = (state, action) => {
  // 对原状态进行浅复制
  const newCart = { ...state };
  // 获取action参数
  const { type, meal } = action;

  switch (type) {
    case "addCart":
      if (newCart.items.indexOf(meal) === -1) {
        newCart.items.push(meal);
        meal.amount = 1;
      } else {
        meal.amount += 1;
      }
      newCart.totalAmount += 1;
      newCart.totalPrice += meal.price;
      return newCart;
    case "removeCart":
      meal.amount -= 1;
      if (meal.amount === 0) newCart.items.splice(newCart.items.indexOf(meal));
      newCart.totalAmount -= 1;
      newCart.totalPrice -= meal.price;
      return newCart;
    case "clearCart":
      newCart.items.forEach((item) => delete item.amount);
      newCart.items = [];
      newCart.totalAmount = 0;
      newCart.totalPrice = 0;
      return newCart;
    default:
      return state;
  }
};

export default cartReducer;
上次编辑于: