React模式指南:重构你的前端思维

大厂技术  高级前端  Node进阶点击上方 程序员成长指北,关注公众号
回复1,加入高级Node交流群

设计模式

复合模式( Compound Pattern )

在我们的应用程序中, 经常包含相互关联的组件, 它们通过共享状态相互依赖, 并共享相关逻辑。你可以在像选择、下拉组件或菜单项这样的组件中经常看到这一点。

复合组件模式允许你创建一同工作以完成需求的组件

我们来看一个例子,这里我构建了一个Counter计数器组件,并在App.js中使用它:

import Counter from "./Counter";

export default function App() {
  return (
    <div>
      <h1>Compound Component Pattern</h1>
      <Counter
        iconIncrease="+"
        iconDecrease="-"
        label="My NOT so flexible counter"
        hideLabel={false}
        hideIncrease={false}
        hideDecrease={false}
      />
    </div>
  );
}

可以看到, 这个Counter组件接收了大量的props用来控制内部元素的显示与隐藏, 且组件不是很灵活, 比如, 我们想更改label元素的位置, 就只能从Counter组件内部修改,但此时所有使用Counter组件的label元素位置都将发生变化

现在让我们使用复合模式重构一下:

复合模式是使用React的Context API���现的, 我们需要使用Context API共享Counter组件中的状态, 编写新的组件并将其添加到Counter组件上。

第一步:创建Context和父组件,并提供子组件所需状态

import { createContext, useContext, useState } from "react";

const CounterContext = createContext();

export default function Counter({ children }) {
  const [count, setCount] = useState(0);

  const increase = () => setCount((c) => c + 1);
  const decrease = () => setCount((c) => c - 1);

  return (
    <CounterContext.Provider value={{ count, increase, decrease }}>
      <span>{children}</span>
    </CounterContext.Provider>
  );
}

第二步:创建子组件用于实现通用功能

function Label({ children }) {
  return <span>{children}</span>;
}
function Count() {
  const { count } = useContext(CounterContext);
  return <span>{count}</span>;
}
function Increase({ icon }) {
  const { increase } = useContext(CounterContext);
  return <button onClick={increase}>{icon}</button>;
}
function Decrease({ icon }) {
  const { decrease } = useContext(CounterContext);
  return <button onClick={decrease}>{icon}</button>;
}

第三步:将子组件作为属性添加到父组件上

jsx
 代码解读
复制代码
Counter.Label = Label;
Counter.Count = Count;
Counter.Increase = Increase;
Counter.Decrease = Decrease;

此时, 我们便可以灵活的使用Counter组件了

jsx
 代码解读
复制代码
import Counter from "./Counter";
import "./styles.css";

export default function App() {
  return (
    <div>
      <h1>Compound Component Pattern</h1>
      <Counter>
        <Counter.Label>My super flexible counter</Counter.Label>
        <Counter.Decrease icon="-" />
        <Counter.Count />
        <Counter.Increase icon="+" />
      </Counter>
    </div>
  );
}

我们可以轻松选择使用哪些子组件, 也能灵活的调换子组件的位置, 且只需要引入Counter组件即可。

完整源码请点击这里[1]

当你在构建组件库时,复合模式非常有用。在使用像 Semantic UI[2] 这样的 UI 库时,你经常会看到这种模式。

高阶组件模式( HOC Pattern )

在我们的应用程序中,我们经常希望在多个组件中使用相同的逻辑。这种逻辑可以包括对组件应用特定样式、要求授权或添加全局状态等。

高阶组件(HOC)是一个接收另一个组件的组件。HOC 包含我们想要应用到作为参数传递的组件上的某些逻辑。应用该逻辑后,HOC 返回具有额外逻辑的元素。

假设我们总是想要在应用程序中的多个组件上添加某种特定样式。我们可以简单地创建一个 HOC,它将样式对象添加到我们传递给它的组件,而不是每次都在本地创建一个样式对象。

我们来看一个例子,我们现在有一个不可更改的ProductList组件用来展示每个产品

function ProductItem({ product }) {
  return (
    <li className="product">
      <p className="product-name">{product.productName}</p>
      <p className="product-price">${product.price}</p>
      <p className="product-description">{product.description}</p>
    </li>
  );
}
// 假设该组件不可更改
function ProductList({ title, items }) {
  return (
    <ul className="list">
      {items.map((product) => (
        <ProductItem key={product.productName} product={product} />
      ))}
    </ul>
  );
}

现在我们又有了新的需求:

  • 一个按钮用来控制列表是否展示

  • 一个按钮用来控制列表折叠(只显示三条)

  • 展示产品的标题

此时我们便可以使用高阶组件,在不修改ProductList组件的情况下完成这三个需求

import { useState } from "react";

//高阶组件通常以 withXXX开头
export default function withToggles(WrappedComponent) {
  return function List(props) {
    // 控制展示
    const [isOpen, setIsOpen] = useState(true);
    // 控制折叠
    const [isCollapsed, setIsCollapsed] = useState(false);

    const displayItems = isCollapsed ? props.items.slice(0, 3) : props.items;

    function toggleOpen() {
      setIsOpen((isOpen) => !isOpen);
      setIsCollapsed(false);
    }

    return (
      <div className="list-container">
        <div className="heading">
          // 展示标题
          <h2>{props.title}</h2>
          <button onClick={toggleOpen}>
            {isOpen ? <span>&or;</span> : <span>&and;</span>}
          </button>
        </div>
        {isOpen && <WrappedComponent {...props} items={displayItems} />}

        <button onClick={() => setIsCollapsed((isCollapsed) => !isCollapsed)}>
          {isCollapsed ? `Show all ${props.items.length}` : "Show less"}
        </button>
      </div>
    );
  };
}

使用时,我们将要添加功能的组件作为参数传入高阶组件中

import withToggle from "./HOC.js";

const ProductListWithToggles = withToggle(ProductList);
export default function App() {
  return (
    <div>
      <h1>Render Props Demo</h1>
      <div className="col-2">
        <ProductListWithToggles
          title="ProductListWithToggle"
          items={products}
        />
      </div>
    </div>
  );
}

完整代码[3]

高阶组件模式允许我们向多个组件提供相同的逻辑,同时将所有逻辑保持在一个地方。

渲染属性模式( Render Props Pattern )

渲染属性是组件上的一个属性,其值是一个返回 JSX 元素的函数。除了渲染属性外,组件本身不渲染任何内容。相反,组件简单地调用渲染属性,而不是实现自己的渲染逻辑。

我们直接来看一个例子:我们现在有一个List组件, 它为列表提供了两个功能

  • 控制列表显示与否

  • 控制列表展开(所有)与显示部分(3条)

现在我们想复用这个组件展示ProductItemCompanyItem, 这时我们便可以使用render prop去代替之前的渲染逻辑

function List({ title, items, render }) {
  const [isOpen, setIsOpen] = useState(true);
  const [isCollapsed, setIsCollapsed] = useState(false);

  const displayItems = isCollapsed ? items.slice(0, 3) : items;

  function toggleOpen() {
    setIsOpen((isOpen) => !isOpen);
    setIsCollapsed(false);
  }

  return (
    <div className="list-container">
      <div className="heading">
        <h2>{title}</h2>
        <button onClick={toggleOpen}>
          {isOpen ? <span>&or;</span> : <span>&and;</span>}
        </button>
      </div>
      // 渲染逻辑
      {isOpen && <ul className="list">{displayItems.map(render)}</ul>}

      <button onClick={() => setIsCollapsed((isCollapsed) => !isCollapsed)}>
        {isCollapsed ? `Show all ${items.length}` : "Show less"}
      </button>
    </div>
  );
}

然后只需在使用List组件时传入对应的渲染逻辑到render属性中即可

export default function App() {
  return (
    <div>
      <h1>Render Props Demo</h1>

      <div className="col-2">
        <List
          title="Products"
          items={products}
          render={(product) => (
            <ProductItem key={product.productName} product={product} />
          )}
        />
        <List
          title="CompanyItem"
          items={companies}
          render={(companie) => (
            <CompanyItem
              key={companie.companyName}
              company={companie}
              defaultVisibility={false}
            />
          )}
        />
      </div>
    </div>
  );
}

作者:前端切图仔_筑基期
链接:https://juejin.cn/post/7301942327818600488
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。# 设计模式

复合模式( Compound Pattern )

在我们的应用程序中, 经常包含相互关联的组件, 它们通过共享状态相互依赖, 并共享相关逻辑。你可以在像选择、下拉组件或菜单项这样的组件中经常看到这一点。

复合组件模式允许你创建一同工作以完成需求的组件

我们来看一个例子,这里我构建了一个Counter计数器组件,并在App.js中使用它:

import Counter from "./Counter";

export default function App() {
  return (
    <div>
      <h1>Compound Component Pattern</h1>
      <Counter
        iconIncrease="+"
        iconDecrease="-"
        label="My NOT so flexible counter"
        hideLabel={false}
        hideIncrease={false}
        hideDecrease={false}
      />
    </div>
  );
}

可以看到, 这个Counter组件接收了大量的props用来控制内部元素的显示与隐藏, 且组件不是很灵活, 比如, 我们想更改label元素的位置, 就只能从Counter组件内部修改,但此时所有使用Counter组件的label元素位置都将发生变化

现在让我们使用复合模式重构一下:

复合模式是使用React的Context API���现的, 我们需要使用Context API共享Counter组件中的状态, 编写新的组件并将其添加到Counter组件上。

第一步:创建Context和父组件,并提供子组件所需状态

import { createContext, useContext, useState } from "react";

const CounterContext = createContext();

export default function Counter({ children }) {
  const [count, setCount] = useState(0);

  const increase = () => setCount((c) => c + 1);
  const decrease = () => setCount((c) => c - 1);

  return (
    <CounterContext.Provider value={{ count, increase, decrease }}>
      <span>{children}</span>
    </CounterContext.Provider>
  );
}

第二步:创建子组件用于实现通用功能

function Label({ children }) {
  return <span>{children}</span>;
}
function Count() {
  const { count } = useContext(CounterContext);
  return <span>{count}</span>;
}
function Increase({ icon }) {
  const { increase } = useContext(CounterContext);
  return <button onClick={increase}>{icon}</button>;
}
function Decrease({ icon }) {
  const { decrease } = useContext(CounterContext);
  return <button onClick={decrease}>{icon}</button>;
}

第三步:将子组件作为属性添加到父组件上

Counter.Label = Label;
Counter.Count = Count;
Counter.Increase = Increase;
Counter.Decrease = Decrease;

此时, 我们便可以灵活的使用Counter组件了

import Counter from "./Counter";
import "./styles.css";

export default function App() {
  return (
    <div>
      <h1>Compound Component Pattern</h1>
      <Counter>
        <Counter.Label>My super flexible counter</Counter.Label>
        <Counter.Decrease icon="-" />
        <Counter.Count />
        <Counter.Increase icon="+" />
      </Counter>
    </div>
  );
}

我们可以轻松选择使用哪些子组件, 也能灵活的调换子组件的位置, 且只需要引入Counter组件即可。

完整源码请点击这里[4]

当你在构建组件库时,复合模式非常有用。在使用像 Semantic UI[5] 这样的 UI 库时,你经常会看到这种模式。

高阶组件模式( HOC Pattern )

在我们的应用程序中,我们经常希望在多个组件中使用相同的逻辑。这种逻辑可以包括对组件应用特定样式、要求授权或添加全局状态等。

高阶组件(HOC)是一个接收另一个组件的组件。HOC 包含我们想要应用到作为参数传递的组件上的某些逻辑。应用该逻辑后,HOC 返回具有额外逻辑的元素。

假设我们总是想要在应用程序中的多个组件上添加某种特定样式。我们可以简单地创建一个 HOC,它将样式对象添加到我们传递给它的组件,而不是每次都在本地创建一个样式对象。

我们来看一个例子,我们现在有一个不可更改的ProductList组件用来展示每个产品

function ProductItem({ product }) {
  return (
    <li className="product">
      <p className="product-name">{product.productName}</p>
      <p className="product-price">${product.price}</p>
      <p className="product-description">{product.description}</p>
    </li>
  );
}
// 假设该组件不可更改
function ProductList({ title, items }) {
  return (
    <ul className="list">
      {items.map((product) => (
        <ProductItem key={product.productName} product={product} />
      ))}
    </ul>
  );
}

现在我们又有了新的需求:

  • 一个按钮用来控制列表是否展示

  • 一个按钮用来控制列表折叠(只显示三条)

  • 展示产品的标题

此时我们便可以使用高阶组件,在不修改ProductList组件的情况下完成这三个需求

import { useState } from "react";

//高阶组件通常以 withXXX开头
export default function withToggles(WrappedComponent) {
  return function List(props) {
    // 控制展示
    const [isOpen, setIsOpen] = useState(true);
    // 控制折叠
    const [isCollapsed, setIsCollapsed] = useState(false);

    const displayItems = isCollapsed ? props.items.slice(0, 3) : props.items;

    function toggleOpen() {
      setIsOpen((isOpen) => !isOpen);
      setIsCollapsed(false);
    }

    return (
      <div className="list-container">
        <div className="heading">
          // 展示标题
          <h2>{props.title}</h2>
          <button onClick={toggleOpen}>
            {isOpen ? <span>&or;</span> : <span>&and;</span>}
          </button>
        </div>
        {isOpen && <WrappedComponent {...props} items={displayItems} />}

        <button onClick={() => setIsCollapsed((isCollapsed) => !isCollapsed)}>
          {isCollapsed ? `Show all ${props.items.length}` : "Show less"}
        </button>
      </div>
    );
  };
}

使用时,我们将要添加功能的组件作为参数传入高阶组件中

import withToggle from "./HOC.js";

const ProductListWithToggles = withToggle(ProductList);
export default function App() {
  return (
    <div>
      <h1>Render Props Demo</h1>
      <div className="col-2">
        <ProductListWithToggles
          title="ProductListWithToggle"
          items={products}
        />
      </div>
    </div>
  );
}

完整代码[6]

高阶组件模式允许我们向多个组件提供相同的逻辑,同时将所有逻辑保持在一个地方。

渲染属性模式( Render Props Pattern )

渲染属性是组件上的一个属性,其值是一个返回 JSX 元素的函数。除了渲染属性外,组件本身不渲染任何内容。相反,组件简单地调用渲染属性,而不是实现自己的渲染逻辑。

我们直接来看一个例子:我们现在有一个List组件, 它为列表提供了两个功能

  • 控制列表显示与否

  • 控制列表展开(所有)与显示部分(3条)

现在我们想复用这个组件展示ProductItemCompanyItem, 这时我们便可以使用render prop去代替之前的渲染逻辑

function List({ title, items, render }) {
  const [isOpen, setIsOpen] = useState(true);
  const [isCollapsed, setIsCollapsed] = useState(false);

  const displayItems = isCollapsed ? items.slice(0, 3) : items;

  function toggleOpen() {
    setIsOpen((isOpen) => !isOpen);
    setIsCollapsed(false);
  }

  return (
    <div className="list-container">
      <div className="heading">
        <h2>{title}</h2>
        <button onClick={toggleOpen}>
          {isOpen ? <span>&or;</span> : <span>&and;</span>}
        </button>
      </div>
      // 渲染逻辑
      {isOpen && <ul className="list">{displayItems.map(render)}</ul>}

      <button onClick={() => setIsCollapsed((isCollapsed) => !isCollapsed)}>
        {isCollapsed ? `Show all ${items.length}` : "Show less"}
      </button>
    </div>
  );
}

然后只需在使用List组件时传入对应的渲染逻辑到render属性中即可

export default function App() {
  return (
    <div>
      <h1>Render Props Demo</h1>

      <div className="col-2">
        <List
          title="Products"
          items={products}
          render={(product) => (
            <ProductItem key={product.productName} product={product} />
          )}
        />
        <List
          title="CompanyItem"
          items={companies}
          render={(companie) => (
            <CompanyItem
              key={companie.companyName}
              company={companie}
              defaultVisibility={false}
            />
          )}
        />
      </div>
    </div>
  );
}
Node 社群



我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。

   “分享、点赞、在看” 支持一波👍
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值