惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
L
Lohrmann on Cybersecurity
aimingoo的专栏
aimingoo的专栏
V
V2EX
S
Security Affairs
T
Threatpost
C
CXSECURITY Database RSS Feed - CXSecurity.com
IT之家
IT之家
J
Java Code Geeks
The Register - Security
The Register - Security
U
Unit 42
C
CERT Recently Published Vulnerability Notes
月光博客
月光博客
A
About on SuperTechFans
H
Hackread – Cybersecurity News, Data Breaches, AI and More
T
The Blog of Author Tim Ferriss
Cisco Talos Blog
Cisco Talos Blog
Project Zero
Project Zero
S
Schneier on Security
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
D
DataBreaches.Net
博客园 - 司徒正美
V
Vulnerabilities – Threatpost
T
Tor Project blog
Security Latest
Security Latest
T
The Exploit Database - CXSecurity.com
T
Threat Research - Cisco Blogs
Scott Helme
Scott Helme
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
M
MIT News - Artificial intelligence
云风的 BLOG
云风的 BLOG
小众软件
小众软件
L
LangChain Blog
Attack and Defense Labs
Attack and Defense Labs
Recent Commits to openclaw:main
Recent Commits to openclaw:main
P
Palo Alto Networks Blog
A
Arctic Wolf
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
C
Cyber Attacks, Cyber Crime and Cyber Security
博客园 - 叶小钗
D
Darknet – Hacking Tools, Hacker News & Cyber Security
L
LINUX DO - 最新话题
MongoDB | Blog
MongoDB | Blog
Webroot Blog
Webroot Blog
H
Hacker News: Front Page
Know Your Adversary
Know Your Adversary
Spread Privacy
Spread Privacy
AWS News Blog
AWS News Blog
Engineering at Meta
Engineering at Meta

BAIYUN'S BLOG

AI Coding 从小工到专家 - BAI YUN 电信 IPTV 内网融合教程:在局域网任意设备观看 IPTV - BAI YUN 美国住宅 IP 提供商体验对比 - BAI YUN Node.js 程序以 Keyless 模式从 1Password 读取密钥 - BAI YUN 高效优雅的观影指南 - BAI YUN 基于 Surge 的 DNS 优化指南 - BAI YUN RIME 鼠须管输入法简明使用教程 - BAI YUN 自动给不同 Git 项目设置不同的用户名邮箱 - BAI YUN Web3 新手如何选择加密货币钱包 - BAI YUN Web3 到底是什么? - BAI YUN 谨慎开启 Safari 的阻止跨站跟踪功能 - BAI YUN iOS Surge 开启后无法使用部分国产 APP 或网络出错的解决办法 - BAI YUN 屏蔽 Telegram 垃圾私信的有效办法 - BAI YUN 每个人都应该读一遍《1984》和《动物庄园》 - BAI YUN iOS 给常见机构号码增加联系人头像 - BAI YUN 在国内如何购买比特币和投资加密货币 - BAI YUN WEB 性能优化:如何让你的网站页面秒开(方法论) - BAI YUN 加密货币投资这件事 - BAI YUN 国内手机号注册推特教程 - BAI YUN 国内注册币安账号并通过 KYC 教程 - BAI YUN 使用一套助记词创建多个以太坊钱包账户 - BAI YUN Apple Watch 的几个用途 - BAI YUN 钱包安全指南:如何保护你的 NFT 和加密资产不被盗 - BAI YUN 我在 macOS 上高频使用的应用 - BAI YUN Web3 时代:如何使用 MetaMask 钱包实现一键登录 - BAI YUN 2022 年了 Safari 无法渲染渐变文字的 BUG 依旧存在 - BAI YUN 境外手机卡:澳门电信大湾区预付卡使用体验 - BAI YUN 解决 iCloud Drive 上传文件卡住的问题 - BAI YUN Electron 应用如何获取系统代理配置 - BAI YUN 解决 iOS 上 Google 账号登录会卡在加载状态的问题 - BAI YUN 记录 Github Copilot 惊讶到我的时刻 - BAI YUN 解决 Xbox 在国内下载速度慢 - BAI YUN 开始使用 IndexNow - BAI YUN Surge iOS 和 AmpliFi 的兼容性问题 - BAI YUN 自建图床指南 - BAI YUN 使用 TypeScript 提升可维护性 - BAI YUN 在 JavaScript 应用中正确的实现时区切换 - BAI YUN 从 Loading 动画出发改进 React 应用的用户体验 - BAI YUN Node.js Tips - BAI YUN 使用 Dart Sass 代替 Node Sass - BAI YUN WSL 踩的坑和解决方法 - BAI YUN NPM & Yarn Tips - BAI YUN Git Tips - BAI YUN macOS 命令行 Tips - BAI YUN JavaScript 防劫持 - BAI YUN Dota2 - BAI YUN macOS 好用的系统维护工具 - BAI YUN 在 Linux 服务器上使用 Puppeteer 生成 PDF - BAI YUN The Golden Circle 黄金圆环法则 - BAI YUN CSS 写法进化史 - BAI YUN 一个 Web 开发者的 Windows10 开发环境设置心得 - BAI YUN Chrome DevTools Tips - BAI YUN Linux & Unix 命令行环境的一些 Tips - BAI YUN 从传统 MVC 到同构:Web 发展历程 - BAI YUN Webpack 动态导入 - BAI YUN JavaScript 中的异步 - BAI YUN 关于我的博客 - BAI YUN
React 设计模式和思想 - BAI YUN
baiyun · 2018-06-21 · via BAIYUN'S BLOG

React 社区在近几年的发展过程中诞生了很多有趣的组件设计 "模式",通过合理的运用这些模式可以让我们写出更好用的 React 组件。

比如,在不使用任何设计模式的情况下,要实现一个类似这样的 Tabs 组件

tabs component

最初的代码可能是这样的

const Tab = ({ value, active, onChange, children }) => (
  <div className={active && 'active'} onClick={evt => onChange(value, evt)}>
    {children}
  </div>
)

用起来是这样的:

class Index extends React.Component {
  state = {
    value: 'a'
  }

  handleChange = value => {
    this.setState({ value })
  }

  render() {
    return (
      <div>
        <Tab value="a" active={this.state.value === 'a'} onChange={this.handleChange}>
          Tab A
        </Tab>
        <Tab value="b" active={this.state.value === 'b'} onChange={this.handleChange}>
          Tab B
        </Tab>
        <Tab value="c" active={this.state.value === 'c'} onChange={this.handleChange}>
          Tab B
        </Tab>
      </div>
    )
  }
}

从上面的代码可以看出,Tab 组件的 Props 中只有 valuechildren 是由使用者决定的,activeonChange 是有固定模式且重复的,我们可以想一些办法将这些有清晰模式且重复的东西自动传给 Tab 组件,避免我们每次都手动维护

复合组件 Compound Components

这种模式利用 React.Children 这个 API, 给每个 Tab 组件自动传入 valueactive props,这样我们使用 Tab 组件时只需要提供 valuechildren 就可以了

const Tabs = ({ value, onChange, onClick, children }) =>
  React.Children.map(children, el => {
    const { value: childValue } = el.props
    return React.cloneElement(el, {
      onChange,
      onClick: onChange,
      active: value === childValue,
      value: childValue
    })
  })

用起来是这样的:

class Index extends React.Component {
  state = {
    value: 'Tab A'
  }

  handleChange = value => {
    this.setState({ value })
  }

  render() {
    return (
      <Tabs value={this.state.value} onChange={this.handleChange}>
        {['Tab A', 'Tab B'].map(tab => (
          <Tab value={tab} key={tab}>
            {tab}
          </Tab>
        ))}
      </Tabs>
    )
  }
}

这种模式一般都会限制 DOM 结构,比如:

  • Tab 组件只能作为 Tabs 的直系子组件,不能被其他组件包裹
  • Tabs 组件只能包含 Tab 组件

Context

针对上面的问题,我们可以用 React 16 新增的 context API 来重写上面的例子

const initialValue = {
  value: '',
  onChange() {}
}

const TabsContext = React.createContext(initialValue)

const Tab = ({ value, children }) => (
  <TabsContext.Consumer>
    {ctx => (
      <div className={ctx.value === value && 'active'} onClick={evt => ctx.onChange(value, evt)}>
        {children}
      </div>
    )}
  </TabsContext.Consumer>
)

const Tabs = ({ value, onChange, children }) => (
  <TabsContext.Provider value={{ value, onChange }}>{children}</TabsContext.Provider>
)

现在可以这样使用:

class Index extends React.Component {
  state = {
    value: 'Tab A'
  }

  handleChange = value => {
    this.setState({ value })
  }

  render() {
    return (
      <Tabs value={this.state.value} onChange={this.handleChange}>
        {['Tab A', 'Tab B'].map(tab => (
          <span key={tab}>
            <Tab value={tab}>{tab}</Tab>
          </span>
        ))}
      </Tabs>
    )
  }
}

上面的代码里 Tabs 组件内可以随意嵌套组合其他组件,并不会影响 Tab 组件正常工作

这种模式主要的优点在于可以 props 透传,可以让 DOM 结构更灵活。

Render Props

Render Props 模式本质上相当于控制反转。在前面的 Tab 组件里可以看到它返回了一个 div 元素,但在有些情况下用户会想要用其他组件来代替这个 div 元素,这种情况可以使用一个 callback 作为 prop,将 Tab 组件的核心状态作为参数传给它,用户拿到这些核心状态后到底是渲染 div 还是渲染其他组件就完全交给用户来决定了

将开头的 Tab 组件修改为如下:

const Tab = ({ value, active, onChange, children, render }) => {
  if (typeof render === 'function') {
    return render({ value, active, onChange })
  }
  return (
    <div className={active && 'active'} onClick={evt => onChange(value, evt)}>
      {children}
    </div>
  )
}

现在可以自定义 render 内容了:

class Index extends React.Component {
  state = {
    value: 'Tab A'
  }

  handleChange = value => {
    this.setState({ value })
  }

  render() {
    return (
      <Tabs value={this.state.value} onChange={this.handleChange}>
        {['Tab A', 'Tab B'].map(tab => (
          <Tab
            value={tab}
            render={({ value, active, onChange }) => (              <li className={active && 'active'} onClick={evt => onChange(value, evt)}>                <span>{tab}</span>              </li>            )}            key={tab}
          />
        ))}
      </Tabs>
    )
  }
}

这种模式可以很好的提取纯粹的逻辑组件,最大化的复用业务逻辑

高阶组件 HOC(Higher Order Component)

所谓高阶组件就是一个函数,它可以接收一个组件并返回一个组件,它可以用来装饰组件,给组件注入 props

例如,我们整个应用中有几个不同地方都散布着几个类似下面这样的的组件:

const NavBar = ({ user }) => (
  <nav>
    User: <span>{user ? user.name : 'fetching...'}</span>
  </nav>
)

这些组件都有一个相同的 user prop,这个 user 是需要从其他地方动态获取的,为了避免获取 user 的操作重复,我们可以把它提取为一个 HOC:

const withUser = WrappedComponent =>
  class HocWithUser extends React.Component {
    state = {
      user: null
    }

    componentDidMount() {
      fetchUser().then(user => {
        this.setState({ user })
      })
    }

    render() {
      return <WrappedComponent {...this.porps} user={this.state.user} />
    }
  }

然后可以这样使用:

const NavBar = ({ user }) => (
  <nav>
    User: <span>{user ? user.name : 'fetching...'}</span>
  </nav>
)
const NavBarWithUser = withUser(NavBar)

class Index extends React.Component {
  render() {
    return <NavBarWithUser />
  }
}

通过这个 HOC 我们可以将 user 注入给指定的组件,而不必重复 user 获取逻辑,但是目前还有两个问题:

  1. withUser 返回的组件在 ReactDevTools 里显示的名称都是相同的 HocWithUser,这会影响我们的 Debug 效率
  2. 调用 withUser 返回的组件是无法访问到 WrappedComponent 的静态属性,这显然是不行的

针对第一个问题,通常做法是在 HocWithUser 增加一个静态属性 displayName,如下:

const withUser = WrappedComponent =>
  class HocWithUser extends React.Component {
    static displayName = `withUser(${WrappedComponent.displayNanme || WrappedComponent.name || 'Component'})`
    state = {
      user: null
    }

    componentDidMount() {
      fetchUser().then(user => {
        this.setState({ user })
      })
    }

    render() {
      return <WrappedComponent {...this.porps} user={this.state.user} />
    }
  }

针对第二个问题,我们可以遍历 WrappedComponent,将它的静态属性添加到 HocWithUser,但是要排除 React 相关的属性,这个操作可以用 hoist-non-react-statics 或者 recompose/hoistStatics 代劳

这里使用后者,将前面的 withUser 作为 hoistStatics 的第一个参数即可:

import hoistStatics from 'recompose/hoistStatics'

const withUser = hoistStatics(
  WrappedComponent =>
    class HocWithUser extends React.Component {
      static displayName = `withUser(${WrappedComponent.displayNanme || WrappedComponent.name || 'Component'})`

      state = {
        user: null
      }

      componentDidMount() {
        fetchUser().then(user => {
          this.setState({ user })
        })
      }

      render() {
        return <WrappedComponent {...this.porps} user={this.state.user} />
      }
    }
)

这里的 user 在实际应用中通常是整个应用共享的,这种情况实际上使用前面提到的 context 模式比较合适,当然 context 模式也可以和 HOC 模式很好的配合

在 controlled 和 uncontrolled 之间切换

在 React 应用里,受控组件也叫木偶组件,就是这个组件的 value 都是父组件控制的,父组件传给它什么内容,它就展示什么,当内容发生变化的时候,通过调用父组件传下来的 onChange 回调函数通知父组件内容发生了变化。

这种模式的好处是可以将展示逻辑和控制逻辑分离,并且可以方便组件之间进行组合复用。

一般情况下受控组件的 props 看起来是这样的

type Props = {
  value: boolean
  onChange: () => void
}

使用的时候通常需要这样:

class Container extends React.Component {
  state = {
    value: false
  }

  handleChange = () => {
    this.setState(prevState => ({ value: !prevState.value }))
  }
  render() {
    return <ControlledComponent value={this.state.value} onChange={this.handleChange} />
  }
}

受控组件必须配合容器组件或者 hooks 来使用,有时候难免会让人觉得啰嗦,因为不是任何场景都需要受控组件,所以我们的目标是让组件又可以受控,也可以不受控,这样使用起来就既灵活也方便。

在上面的例子中可以利用 value 这个 prop 来判断是否应该是一个受控组件,如果 value 存在那就使用这个值,需要改变值得时候就调用 onChange 让父级组件自己决定怎么改变 value 的值。

value 存在的时候这个组件是受控组件,不存在的时候就是非受控组件。

class MyComponent extends React.Component {
  state = {
    value: false
  }

  get value() {
    return 'value' in this.props ? this.props.value : this.state.value
  }

  handleClick = () => {
    const { onChange } = this.props
    if (onChange) {
      onChange()
    } else {
      this.setState(prevState => ({ value: !prevState }))
    }
  }

  render() {
    return <div onClick={this.handleClick}>state: {this.value}</div>
  }
}

上面的模式可以在这里查看 在线 Demo

React Hooks

hoc-vs-renderProps-vs-hooks

从前面的例子可以看出,在 React Hooks 出现之前,逻辑复用主要依靠 HOC 和 Render Props,但是这两种模式本质上都是都过包一层组件来实现的,有一个很致命的问题是嵌套,如果你有10个 HOC 或者 Render Props 实现的逻辑组件,那么当你需要同时使用这 10个逻辑组件的时候,就会出现嵌套地狱的情况。

<CurrentUserQuery>
  {(currentUserQuery) => (
    <SettingsQuery user={currentUserQuery.data}>          
      {(settingsQuery) => (
        <ThemeQuery user={currentUserQuery.data}> 
          {(themeQuery) => (
            <ProjectsQuery user={currentUserQuery.data}>
              {(projectsQuery) => (
                projectsQuery.data &&
                projectsQuery.data.map(p => (
                  <Project {...p} settings={settingsQuery.data} theme={themeQuery.data} />
                  )
              )}
            </ProjectsQuery>
          )}
        </UserThemeQuery>
      )}
    </UserSettingsQuery>
  )}
</CurrentUserQuery>

除此之外,在 React Hooks 之前,函数式组件并没有生命周期的概念,所以副作用和生命周期都得放到 class 组件中完成,但是 class 组件的生命周期在使用上也是有很多痛点和心智负担的,比如在 didMount 里面监听了一个事件,那么你要记得在 unmount 生命周期里清除这个事件监听,否则就容易造成内存泄露等问题。还有在 class 组件里面监听一个值的变化也非常麻烦不直观,你需要在 didUpdate 里面手段做很多判断逻辑。

因为以上多种问题,最终 React 团队在 16.8 这个版本正式推出了革命性的 React Hooks,它不仅可以让传统的函数式组件具备 class 组件的能力,同时更有意义的是,它让 React 更加函数式,在一定程度上改变了开发者的思维习惯,让组件的状态和副作用之间的同步关系更加清晰明确。当然,Hooks 也不是银弹,它也带来了额外的一些问题。

hooks 参考资料:

本文原载于:baiyun.me

原文链接:https://baiyun.me/react-design-patterns