【译】在 React 中拥抱函数——无状态函数式组件及其重要性

原文:Embracing Functions in React - Stateless Functional Components and Why they Matter
说明:因个人水平有限,如有误译,欢迎指正。

介绍

本篇文章不是向您介绍任何最佳实践或者使用 React 编写应用的某个“唯一方式”。

本文讲述的都是关于 React 中的无状态函数式组件 (stateless functional component) 以及为什么它们可能有用或为什么应首先受到考虑。

定义

在我们探讨这个问题之前,我们先了解一下在 React 上下文中函数式组件的定义。它本质上就是一个常规的函数,接收一个 props 并返回一个元素。

function Item(props) {
  return (
    <div className='item'>
      {props.title}
      <span
        className='deleteItem'
        onClick={props.remove(props.id)}
      > x </span>
    </div>
  )
}

使用 ES6 的箭头函数解构,我们也可以这样编写:

const Item = ({ id, title, remove }) => <div className='item'>
  {title}
  <span
    className='deleteItem'
    onClick={remove(id)}
  > x </span>
</div>

初看,这与使用 createClassES6 Class 并没有什么特别之处,无状态函数式组件仅仅是一种编写风格。但是从我的观点来看却有很多不同。

现在让我们看一下函数式组件和基于类的方式的不同之处,并根据给定的事实,我们能推导出什么有价值的内容。

无生命周期方法

函数式组件,有时也被称为无状态组件,没有任何生命周期方法,意味着每次上层组件树状态发生变更时它们都会重新渲染,这就是因为缺少 shouldComponentUpdate 方法导致的。这也同样意味着您不能定义某些基于组件挂载和卸载的行为。

没有 this 和 ref

更有趣的是您在函数式组件中既不能使用 this 关键字或访问到 ref。对于习惯了严格意义上的类或面向对象风格的人来说,这很让他们惊讶。这也是使用函数最大的争论点。

另一个有趣的事实就是您仍然可以访问到 context如果您将 context 定义为函数的一个 props

为何甚至将函数认为实际可行的方式

所以您可能会问自己,它的优势究竟体现在哪里。特别是您已经使用了 React 并倾向于使用基于类的方式。当我们拥抱这个概念的时候,容器型组件 (container component)展示型组件 (presentational component) 的概念就会变得非常清晰。您也可以阅读 Dan Abramov 关于此主题的 文章 以获取更深的了解。

通过将逻辑和数据处理与 UI 展示剥离开来,我们就可以避免在展示型组件中处理任何的状态。无状态函数式组件强制实施了这样的风格,因为您无论如何都没有办法处理本地状态了。它强制您将任何的状态处理移交至上层的组件树,而让下层的组件只做它们所做的——关注 UI 的展示。

没有逻辑意味着相同的表示具有相同的数据。

避免常见陷阱

在编写无状态函数式组件时,您需要避免某些特定的模式。避免在函数式组件中定义函数,这是因为每一次函数式组件被调用的时候,一个新的函数都会被创建。

const Form = ({...}) => {
  const handleSomething = e => path(['event', 'target'], e)
  return (
    // ...
  )
}

这个问题很容易解决,您可以将这个函数作为 props 传递进去,或者将它定义在组件外面。

const handleSomething = e => path(['event', 'target'], e)
const Form = ({...}) => // ...

有时候谈起无状态函数式组件会提到纯 (pure) 这个词。在这方面您应该避免使用 context 或 defaultProps,如果您需要定义上述任何一个或两个,您应该选择基于类的方式。

const ListComponent = ({...})

ListComponent.contextTypes = {
  style: React.PropTypes.object.isRequired
}

ListComponent.defaultProps = {
  items: []
}

至于 defaultProps,一个变通的方案就是使用默认参数。

const ListComponent = ({ items = [] }) => (...)

关于纯函数,请查看 Bernhard Gschwantner评论,他总结得非常完美。

另一个常见陷阱就是简单地认为使用纯无状态函数式组件可以获得性能上的提升。这个观点是不正确的。相反,当我们需要处理大量无状态函数型组件的时候,它的对立观点却是正确的。

性能问题是由于缺少生命周期方法导致的,这就意味着 React 没有访问任何额外的方法并且总是渲染组件。

缺少声明周期方法在另一方面也导致了没法定义 shouldComponentUpdate 方法。因此,我们不能够告诉 React 是重新渲染还是不渲染,这也就导致了永远都会重新渲染。接下来我们将会了解到缓解这种问题的方法。

高阶组件

如果您想了解更多高阶组件以及使用它们的优点,请查看 Why The Hipsters Recompose Everything

高阶组件是一种接收组件为参数并返回一个新的组件的函数。

HOC :: Component -> Component

这种方式可以让我们解决许多因使用无状态函数式组件而导致的问题。简言之,我们可以将函数式组件封装进高阶组件以解决状态处理和渲染优化这样的问题,高阶组件可以帮助我们关注本地状态处理以及 shouldComponentUpdate 函数的实现。

Recompose 就帮我们解决了以上提到的情况。

下面的这个例子是直接从这个项目 README 文件中拿来的。

// 渲染代价较高的组件
const ExpensiveComponent = ({ propA, propB }) => {...}

// 与 React's PureRenderMixin 作用相同
const OptimizedComponent = pure(ExpensiveComponent)

// 继续优化:仅在特定 prop 键值发生变化才更新
const HyperOptimizedComponent =
  onlyUpdateForKeys(['propA', 'propB'])(ExpensiveComponent)

正如上面所示,我们可以将精力集中于 UI 表示并在需要的时候将函数封装进一个纯函数并导出这个封装的函数。我们就不需要将原始的函数重构为一个类组件。

下面的示例来自于 Why The Hipsters Recompose Everything

const withState = (stateName, stateUpdateFn, initialState) => {
  return Comp => {
    const factory = createFactory(Comp)
    return createClass({
      getInitialState() { return { value: initialState } },
      stateUpdateFn(fn) {
        this.setState(({ value } ) => ({ value: fn(value) }))
      },
      render() {
        return factory({
          ...this.props,
          [stateName] : this.state.value,
          [stateUpdateFn] : this.stateUpdateFn,
        })
      }
    })
  }
}

withState 使得我们在有需要的时候管理本地组件状态,只需将我们的无状态函数式组件传递给 enhance 函数即可。

const enhance = withState('counter', 'setCounter', 0)
const Counter = enhance(({ counter, setCounter }) =>
  <div>
    Count: {counter}
    <button onClick={() => setCounter(n => n+1)}>Increment</button>
    <button onClick={() => setCounter(n => n-1)}>Decrement</button>
  </div>
)

同样,recompose 已经实现了 withState,所以就没有必要自己再去实现它了。

结束

使用无状态函数式组件最大的好处就是它能够将容器型和展示型组件明确区分开来,避免产生大型以及杂乱的组件。没有 this 关键字也就意味着没有快捷方式在整个应用中随机地展开状态。

当一个开发团队中人员经验存在差别时,这些方面就会变得异常有用,它会帮助我们间接地执行内部开发的标准。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容