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

本文参考 Patterns.dev

Patterns.dev 是一个免费的在线资源,提供了使用原生 JavaScript 或现代框架(VueReact)构建功能强大的 Web 应用程序的设计、渲染和性能模式

设计模式

复合模式( Compound Pattern )

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

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

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

jsx 复制代码
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和父组件,并提供子组件所需状态

jsx 复制代码
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>
  );
}

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

jsx 复制代码
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组件即可。

完整源码请点击这里

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

高阶组件模式( HOC Pattern )

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

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

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

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

jsx 复制代码
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组件的情况下完成这三个需求

jsx 复制代码
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>
    );
  };
}

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

js 复制代码
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>
  );
}

完整代码

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

渲染属性模式( Render Props Pattern )

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

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

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

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

jsx 复制代码
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属性中即可

jsx 复制代码
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>
  );
}

完整代码

相关推荐
newxtc29 分钟前
【爱给网-注册安全分析报告-无验证方式导致安全隐患】
前端·chrome·windows·安全·媒体
dream_ready1 小时前
linux安装nginx+前端部署vue项目(实际测试react项目也可以)
前端·javascript·vue.js·nginx·react·html5
编写美好前程1 小时前
ruoyi-vue若依前端是如何防止接口重复请求
前端·javascript·vue.js
flytam1 小时前
ES5 在 Web 上的现状
前端·javascript
喵喵酱仔__1 小时前
阻止冒泡事件
前端·javascript·vue.js
GISer_Jing1 小时前
前端面试CSS常见题目
前端·css·面试
八了个戒2 小时前
【TypeScript入坑】什么是TypeScript?
开发语言·前端·javascript·面试·typescript
不悔哥2 小时前
vue 案例使用
前端·javascript·vue.js
anyup_前端梦工厂3 小时前
Vuex 入门与实战
前端·javascript·vue.js
你挚爱的强哥3 小时前
【sgCreateCallAPIFunctionParam】自定义小工具:敏捷开发→调用接口方法参数生成工具
前端·javascript·vue.js