21. React Fragment:简化您的组件结构

初始代码(点击展开)
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";

const pizzaData = [
  {
    name: "Focaccia",
    ingredients: "Bread with italian olive oil and rosemary",
    price: 6,
    photoName: "pizzas/focaccia.jpg",
    soldOut: false,
  },
  {
    name: "Pizza Margherita",
    ingredients: "Tomato and mozarella",
    price: 10,
    photoName: "pizzas/margherita.jpg",
    soldOut: false,
  },
  {
    name: "Pizza Spinaci",
    ingredients: "Tomato, mozarella, spinach, and ricotta cheese",
    price: 12,
    photoName: "pizzas/spinaci.jpg",
    soldOut: false,
  },
  {
    name: "Pizza Funghi",
    ingredients: "Tomato, mozarella, mushrooms, and onion",
    price: 12,
    photoName: "pizzas/funghi.jpg",
    soldOut: false,
  },
  {
    name: "Pizza Salamino",
    ingredients: "Tomato, mozarella, and pepperoni",
    price: 15,
    photoName: "pizzas/salamino.jpg",
    soldOut: true,
  },
  {
    name: "Pizza Prosciutto",
    ingredients: "Tomato, mozarella, ham, aragula, and burrata cheese",
    price: 18,
    photoName: "pizzas/prosciutto.jpg",
    soldOut: false,
  },
];

function App() {
  return (
    <div className="container">
      <Header />
      <Menu />
      <Footer />
    </div>
  );
}

function Header() {
  const style = {};

  return (
    <header className="header">
      <h1 style={style}>Fast React Pizza Co.</h1>
    </header>
  );
}

function Menu() {
  const pizzas = pizzaData;

  // const pizzas = [];

  const numPizzas = pizzas.length;

  return (
    <main className="menu">
      <h2>Our Menu</h2>

      {numPizzas > 0 ? (
        <ul className="pizzas">
          {pizzas.map((pizza) => (
            <Pizza pizzaObj={pizza} key={pizza.name} />
          ))}
        </ul>
      ) : (
        <p>We're still working on our menu. Please come back later</p>
      )}

      {/* <Pizza
        name="Pizza Spinaci"
        ingredient="Tomato, mozarella, spinach, and ricotta cheese"
        photoName="pizzas/spinaci.jpg"
        price={10}
      />
      <Pizza
        name="Pizza Funghi"
        ingredient="Tomato, mushrooms"
        price={12}
        photoName="pizzas/funghi.jpg"
      /> */}
    </main>
  );
}

function Pizza(props) {
  console.log(props);

  if (props.pizzaObj.soldOut) {
    return null;
  }

  return (
    <li className="pizza">
      <img src={props.pizzaObj.photoName} alt={props.pizzaObj.name}></img>
      <div>
        <h3>{props.pizzaObj.name}</h3>
        <p>{props.pizzaObj.ingredients}</p>
        <span>{props.pizzaObj.price}</span>
      </div>
    </li>
  );
}

function Footer() {
  const hour = new Date().getHours();
  const openHour = 20;
  const closeHour = 22;
  const isOpen = hour >= openHour && hour <= closeHour;

  console.log(isOpen);

  return (
    <footer className="footer">
      {isOpen ? (
        <Order closeHour={closeHour} />
      ) : (
        <p>
          We're happy to welcome you between {openHour}:00 and {closeHour}
        </p>
      )}
    </footer>
  );
}

function Order(props) {
  return (
    <div className="order">
      <p>
        We're open until {props.closeHour}:00. Come visit us or order online.
      </p>
      <button className="btn">Order</button>
    </div>
  );
}

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

什么是 React Fragment 以及为什么需要它?

在 React 开发中,我们经常会遇到需要返回多个并列的 JSX 元素而不想添加额外包装元素的情况。这正是 React Fragment 发挥作用的时候。

React 的 JSX 规则要求每个组件只能返回一个根元素。通常,我们会用一个 <div> 来包裹多个元素,但这可能会导致 DOM 树中出现不必要的嵌套层级,影响样式和语义。

问题示例

让我们看看比萨店菜单应用程序中的一个场景。在 Menu 组件中,我们想要在有比萨的情况下显示一段介绍文字和比萨列表,但在没有比萨的情况下只显示一条提示信息:

function Menu() {
  const pizzas = pizzaData;
  const numPizzas = pizzas.length;

  return (
    <main className="menu">
      <h2>Our Menu</h2>

      {numPizzas > 0 ? (
        // 这里我们想要返回段落和列表,但不能直接并列
        <p>我们提供多种美味的比萨选择,全部使用新鲜原料制作。</p>
        <ul className="pizzas">
          {pizzas.map((pizza) => (
            <Pizza pizzaObj={pizza} key={pizza.name} />
          ))}
        </ul>
      ) : (
        <p>We're still working on our menu. Please come back later</p>
      )}
    </main>
  );
}

但是,上面的代码会报错:”JSX expressions must have one parent element”(JSX 表达式必须有一个父元素)。

解决方案:React Fragment

我们可以使用 React Fragment 来解决这个问题。Fragment 允许我们将多个元素组合在一起,而不会在 DOM 中留下任何痕迹。

简写语法

最常用的 Fragment 语法非常简洁:

{numPizzas > 0 ? (
  <>
    <p>我们提供多种美味的比萨选择,全部使用新鲜原料制作。</p>
    <ul className="pizzas">
      {pizzas.map((pizza) => (
        <Pizza pizzaObj={pizza} key={pizza.name} />
      ))}
    </ul>
  </>
) : (
  <p>We're still working on our menu. Please come back later</p>
)}

这里的 <>…</> 就是 Fragment 的简写形式。它不会在 DOM 中创建任何额外的元素,只是将其中的内容组合在一起。

完整语法

如果需要为 Fragment 添加属性(如 key),则需要使用完整语法:

{numPizzas > 0 ? (
  <React.Fragment key="menu-items">
    <p>我们提供多种美味的比萨选择,全部使用新鲜原料制作。</p>
    <ul className="pizzas">
      {pizzas.map((pizza) => (
        <Pizza pizzaObj={pizza} key={pizza.name} />
      ))}
    </ul>
  </React.Fragment>
) : (
  <p>We're still working on our menu. Please come back later</p>
)}

这需要确保我们已经导入了 React:import React from “react”;。

何时使用 Fragment?

Fragment 在以下情况特别有用:

  1. 当你需要返回多个并列元素时
  2. 在列表渲染中需要包装多个元素但不想添加额外的 DOM 节点
  3. 在条件渲染中需要返回多个元素

检查 DOM 结构

使用 Fragment 的一个好处是保持 DOM 结构的清洁。如果我们检查页面的 DOM 树,会发现 Fragment 内的元素直接出现在父元素中,而没有额外的包装层:

<main class="menu">
  <h2>Our Menu</h2>
  <!-- 下面的元素直接作为 main 的子元素,中间没有额外的 div -->
  <p>我们提供多种美味的比萨选择,全部使用新鲜原料制作。</p>
  <ul class="pizzas">
    <!-- 列表项内容 -->
  </ul>
</main>

更广泛的应用

Fragment 不仅可以用于条件渲染,还可以用于组件的根元素。例如,我们可以修改 App 组件,让它直接返回三个组件,而不是将它们包裹在一个容器中:

function App() {
  return (
    <>
      <Header />
      <Menu />
      <Footer />
    </>
  );
}

这样,这三个组件会直接成为渲染目标的子元素,没有额外的包装层。

结论

React Fragment 是一个简单但非常有用的功能,它解决了 JSX 只能有一个根元素的限制,同时保持了 DOM 结构的清洁。通过使用 Fragment,我们可以创建更灵活、更语义化的组件结构,不必担心添加不必要的 DOM 元素。

在实际开发中,Fragment 已成为 React 开发者工具箱中不可或缺的工具。无论是条件渲染、列表渲染还是组件结构优化,Fragment 都能帮助我们写出更优雅的代码。

完整代码(点击展开)
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";

const pizzaData = [
  {
    name: "Focaccia",
    ingredients: "Bread with italian olive oil and rosemary",
    price: 6,
    photoName: "pizzas/focaccia.jpg",
    soldOut: false,
  },
  {
    name: "Pizza Margherita",
    ingredients: "Tomato and mozarella",
    price: 10,
    photoName: "pizzas/margherita.jpg",
    soldOut: false,
  },
  {
    name: "Pizza Spinaci",
    ingredients: "Tomato, mozarella, spinach, and ricotta cheese",
    price: 12,
    photoName: "pizzas/spinaci.jpg",
    soldOut: false,
  },
  {
    name: "Pizza Funghi",
    ingredients: "Tomato, mozarella, mushrooms, and onion",
    price: 12,
    photoName: "pizzas/funghi.jpg",
    soldOut: false,
  },
  {
    name: "Pizza Salamino",
    ingredients: "Tomato, mozarella, and pepperoni",
    price: 15,
    photoName: "pizzas/salamino.jpg",
    soldOut: true,
  },
  {
    name: "Pizza Prosciutto",
    ingredients: "Tomato, mozarella, ham, aragula, and burrata cheese",
    price: 18,
    photoName: "pizzas/prosciutto.jpg",
    soldOut: false,
  },
];

function App() {
  return (
    <div className="container">
      <Header />
      <Menu />
      <Footer />
    </div>
  );
}

function Header() {
  const style = {};

  return (
    <header className="header">
      <h1 style={style}>Fast React Pizza Co.</h1>
    </header>
  );
}

function Menu() {
  const pizzas = pizzaData;

  // const pizzas = [];

  const numPizzas = pizzas.length;

  return (
    <main className="menu">
      <h2>Our Menu</h2>

      {numPizzas > 0 ? (
        <>
          <p>我们提供多种美味的比萨选择,全部使用新鲜原料制作。</p>
          <ul className="pizzas">
            {pizzas.map((pizza) => (
              <Pizza pizzaObj={pizza} key={pizza.name} />
            ))}
          </ul>
        </>
      ) : (
        <p>We're still working on our menu. Please come back later</p>
      )}

      {/* <Pizza
        name="Pizza Spinaci"
        ingredient="Tomato, mozarella, spinach, and ricotta cheese"
        photoName="pizzas/spinaci.jpg"
        price={10}
      />
      <Pizza
        name="Pizza Funghi"
        ingredient="Tomato, mushrooms"
        price={12}
        photoName="pizzas/funghi.jpg"
      /> */}
    </main>
  );
}

function Pizza(props) {
  console.log(props);

  if (props.pizzaObj.soldOut) {
    return null;
  }

  return (
    <li className="pizza">
      <img src={props.pizzaObj.photoName} alt={props.pizzaObj.name}></img>
      <div>
        <h3>{props.pizzaObj.name}</h3>
        <p>{props.pizzaObj.ingredients}</p>
        <span>{props.pizzaObj.price}</span>
      </div>
    </li>
  );
}

function Footer() {
  const hour = new Date().getHours();
  const openHour = 20;
  const closeHour = 22;
  const isOpen = hour >= openHour && hour <= closeHour;

  console.log(isOpen);

  return (
    <footer className="footer">
      {isOpen ? (
        <Order closeHour={closeHour} />
      ) : (
        <p>
          We're happy to welcome you between {openHour}:00 and {closeHour}
        </p>
      )}
    </footer>
  );
}

function Order(props) {
  return (
    <div className="order">
      <p>
        We're open until {props.closeHour}:00. Come visit us or order online.
      </p>
      <button className="btn">Order</button>
    </div>
  );
}

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


Comments

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注