轻松将 Ramda 集成到您的 React 工作流中

2025-06-08

轻松将 Ramda 集成到您的 React 工作流中

最初发表于Hint 的博客

在 Hint,我们经常使用 React 编写用户界面。我们喜欢它的声明式 API、易于团队沟通和协作的思维模型,尤其是最近新增的 hooks。然而,React 并没有提供完整的工具包。它缺少一些开箱即用的功能:数据获取、异步函数处理、以实用的方式应用样式等等。

在我学习 React 的过程中,我发现 React 功能集中最大的漏洞其实是 JavaScript 本身的问题。相比 Ruby 或 Elixir 等其他依赖大量工具包的语言,JavaScript 提供的可用资源并不多。我开始编写自己的辅助库,直到一位朋友告诉我关于 Ramda 的信息。Ramda的主页上是这样写的:

面向 JavaScript 程序员的实用函数库。

嘿!我喜欢函数式的东西、库、JavaScript……而且我还是个程序员!我对它一见钟情(不,我一点也不觉得羞耻)。

Ramda 的第一个障碍是函数式编程。如果你从未涉足函数式编程领域,请阅读Randy Coulman 的“Thinking in Ramda”系列,非常精彩。

作为一名 React 开发者,Ramda 的第二个挑战是如何有效地将它与 React 结合使用。我仍在学习和尝试这两个库如何协同工作,我想分享一些过去几年我坚持的模式。让我们开始吧!

isNil使用And让你的代码更易读isEmpty

有时候,React 代码的可读性并不好。我认为,后 Hook 时代的情况更糟。组件主体中添加的逻辑越来越多,而且没有生命周期方法可以自动组织代码render,因此我只能尽力清理代码。

Ramda 的isNilisEmpty是让你的组件主体炫目的一个很好的开始🕺。例如:

  const Entry = ({ client }) => (
    <Query query={currentUserQuery}>
      {({ loading, data }) => {
        if (!loading && !data.user.posts)
          return <NoPosts />

        if (data.user) {
          setErrorTrackingContext(data.user)
          getPostMetaData(data.user, client)
        }

        return (
          // code that renders things here
        )
      }}
    </Query>
  )

关于代码示例的说明:本文中的所有代码均基于我编写的真实代码。文中引用了一些Hint 非常喜欢的Apollo React 库。为了简洁起见,大多数导入代码均已删除。本文没有博客文章式的、fooBar填充式的、伪代码。几乎可以投入生产™。

注意第一点if:如果加载完成且data.user.posts为假值,我们会提前返回一个组件。第二点if:如果有用户,让我们设置正在使用的错误跟踪上下文(提示:我们喜欢Honeybadger),然后获取一些帖子元数据。我们先不考虑这些函数的具体实现,专注于逻辑。乍一看,情况还不错——但“还不错”并非标准。卓越才是!让我们再来一次,不过使用 Ramda:

  import { isNil, isEmpty } from 'ramda'

  const Entry = ({ client }) => (
    <Query query={currentUserQuery}>
      {({ loading, data }) => {
        if (isNil(loading) && isEmpty(data.user.posts))
          return <NoPosts />

        if (data.user) {
          setErrorTrackingContext(data.user)
          getPostMetaData(data.user, client)
        }

        return (
          // code that renders things here
        )
      }}
    </Query>
  )

注意顶部的 ,以及对第一个.import的更新,如果或 ,则会返回。这个函数非常有用,因为它不仅仅检查值是否为,这本质上就是它之前所做的()。后腿免于一个讨厌的虫子!ifisNiltrueloadingnullundefinedfalsy!loading

在同一行,如果传入的值是或 ,则isEmpty返回。使用 GraphQL 时,如果你请求一个集合,但没有任何值,通常会返回一个空数组。我们之前的逻辑检查,也可能引入了一个意想不到的错误!Hindquarters 再次得救了。true''[]{}!data.user.posts

专业提示

第一点,已经是专业提示了?今天是个好日子。

Ramda 由许多具有特定用途的小函数组成。将它们组合在一起,你就能创造出一些有趣的东西!让我们创建一个与 逆相关的助手isNil

  import { isNil, isEmpty, complement } from 'ramda'

  const isPresent = complement(isNil)

  const Entry = ({ client }) => (
    <Query query={currentUserQuery}>
      {({ loading, data }) => {
        if (isNil(loading) && isEmpty(data.user.posts))
          return <NoPosts />

        if (isPresent(data.user)) {
          setErrorTrackingContext(data.user)
          getPostMetaData(data.user, client)
        }

        return (
          // code that renders things here
        )
      }}
    </Query>
  )

complement它的第一个参数是一个函数,第二个参数是一个值。如果调用时返回一个假值,则输出为true(反之亦然)。使用complement可以使我们的第二个参数if更美观一些。

你可能会说:“这真的很简单。为什么 Ramda 没有自带这样的辅助函数呢?” 可以把 Ramda 函数想象成一个个独立的乐高积木。它们单独使用时功能不多,但把它们组合起来,就能创造出非常有用的东西。如果你想要一套更“全面的实用程序”,可以看看Ramda Adjunct

单独操作对象很危险!以下函数proppath

如果你明白标题笑话的意思,网络积分+1

作为开发者,没有什么比深度访问对象更可怕的了。如果这还不让你感到畏缩的话:

if (foo.bar.baz.theLastPropertyIPromise.justKiddingOneMore) doTheThing()

那我们需要谈谈。如果你提出的解决方案是:

if (
  foo &&
  foo.bar &&
  foo.bar.baz &&
  foo.bar.baz.theLastPropertyIPromise &&
  foo.bar.baz.theLastPropertyIPromise.justKiddingOneMore
)
  doTheThing()

那我们确实需要谈谈。

玩笑归玩笑,我们都经历过这种情况。很容易完全忽略复杂的检查,或者编写占用过多字节且难以阅读的条件语句。Ramda 为我们提供了proppath来安全地访问对象。让我们看看它们是如何工作的:

import { prop, path, pipe } from 'ramda'

const obj = { foo: 'bar', baz: { a: 1, b: 2 } }

const getFoo = prop('foo')
getFoo(obj) // => 'bar'

const getBazA = path(['baz', 'a'])
getBazA(obj) // => 1

太棒了!“但这又怎么安全呢?你要求的所有属性都存在!”很高兴你问了这个问题:

import { path, pipe } from 'ramda'

const obj = { foo: 'bar', baz: { a: 1, b: 2 } }

const getSomethingThatDoesNotExist = path([
  'foo',
  'bar',
  'baz',
  'theLastPropertyIPromise',
  'justKiddingOneMore'
])
getSomethingThatDoesNotExist(obj) // => undefined

感谢 Ramda!Hindquarters 又一次得救了。注意undefined,返回的是一个假值。这对于存在性检查非常有用!让我们将新学到的知识应用到我们的<Entry />组件中:

  import { isNil, isEmpty, complement, prop } from 'ramda'

  const getUser = prop('user')
  const userIsPresent = pipe(
    getUser,
    complement(isNil)
  )

  const Entry = ({ client }) => (
    <Query query={currentUserQuery}>
      {({ loading, data }) => {
        if (isNil(loading) && isEmpty(data.user.posts))
          return <NoPosts />

        if (userIsPresent(data)) {
          const user = getUser(data)
          setErrorTrackingContext(user)
          getPostMetaData(user, client)
        }

        return (
          // code that renders things here
        )
      }}
    </Query>
  )

看起来确实好多了。第二个条件还可以进一步重构if。为了好玩,看看你能不能用 Ramda 把它合并if成一个函数。答案就在文章末尾!

准备道具evolve

将组件 props 转换为有用的内容是常见的做法。让我们来看这个例子,其中我们连接了姓和名,并格式化了日期:

const NameAndDateDisplay = ({ date, firstName, lastName }) => (
  <>
    <div>
      Hello {firstName.toUpperCase()} {lastName.toUpperCase()}!
    </div>
    <div>It is {dayjs(date).format('M/D/YYYY dddd')}</div>
  </>
)

这段代码看似简单,但其实有些蹊跷。你能发现吗?问题在于它太过简单了。处理真实数据、真实 API 以及人类编写的真实代码时,事情并不总是那么简单。有时,你的项目会使用第三方 API,而你无法完全控制从服务器返回的内容。

在这些情况下,我们倾向于将所有逻辑都放入组件主体中,如下所示:

const NameAndDateDisplay = ({ date, firstName, lastName }) => {
  const formattedDate = formatDate(date)
  const formattedFirstName = formatFirstName(firstName)
  const formattedLastName = formatLastName(lastName)

  return (
    <>
      <div>
        Hello {firstName} {lastName}!
      </div>
      <div>It is {formattedDate}</div>
    </>
  )
}

这带来了一些问题。一些非常重要的逻辑被绑定到组件主体中,这给测试带来了困难。测试这些格式化程序的唯一方法是渲染组件。此外,这确实使组件主体臃肿不堪。在 Rails 中,你会听到“胖模型,瘦控制器”的说法;在 React 中,类似的术语是“胖助手,瘦组件主体”。

幸运的是,Ramdaevolve确实可以帮助我们。evolve它接受两个参数;第一个参数是其值为函数的对象,第二个参数是您想要操作的对象。

import { evolve, toUpper } from 'ramda'

evolve({ foo: toUpper }, { foo: 'weeee' })
// => { foo: 'WEEEE' }

太棒了!有两点需要注意evolve:它是递归的,并且它不会对第一个参数中未指定的值进行运算。

import { evolve, toUpper, add } from 'ramda'

const format = evolve({
  foo: toUpper,
  numbers: { a: add(2) },
  dontTouchMe: 'foobar'
})
format({ foo: 'weeee', numbers: { a: 3 } })
// => { foo: 'WEEEE', numbers: { a: 5 }, dontTouchMe: 'foobar' }

有了这些新知识,让我们重构我们的组件:

import { evolve, pipe } from 'ramda'

const prepProps = evolve({
  date: formatDate,
  firstName: formatFirstName,
  lastName: formatLastName
})

const NameAndDateDisplay = ({ date, firstName, lastName }) => (
  <>
    <div>
      Hello {firstName} {lastName}!
    </div>
    <div>It is {date}</div>
  </>
)

export default pipe(
  prepProps,
  NameAndDateDisplay
)

太棒了!我们成功地将格式化代码与渲染代码分离了。

总结

React 和 Ramda 都是非常强大的工具。了解它们的工作原理和交互方式可以简化并加快开发速度。

以后,当你需要将辅助库从一个项目复制粘贴到另一个项目时,请务必记住 Ramda。很有可能,Ramda 函数可以完成相同的任务,甚至更多!本文未涵盖更多 Ramda 函数。请参阅Ramda 文档了解更多信息。

重构答案

我们的第二个if条件,完全重构:

// setErrorTrackingContextAndGetPostMetaData.js
import { prop, pipe, complement, when, converge, curry, __ } from 'ramda'

const getUser = prop('user')
const userIsPresent = pipe(
  getUser,
  complement(isNil)
)
const curriedGetPostMetaData = curry(getPostMetaData)

const setErrorTrackingContextAndGetPostMetaData = client =>
  when(
    userIsPresent,
    converge(getUser, [
      setErrorTrackingContext,
      curriedGetPostMetaData(__, client)
    ])
  )

export default setErrorTrackingContextAndGetPostMetaData

// Entry.js
// in the body of <Entry />

// ...
setErrorTrackingContextAndGetPostMetaData(client)(data)
// ...
鏂囩珷鏉ユ簮锛�https://dev.to/hint/easily-integrate-ramda-into-your-react-workflow-3kh4
PREV
如何回答科技人才面试题“自我介绍”
NEXT
使用 Telegram 来描述或渲染 IP。