React 中的 8 种可能导致应用崩溃的做法

2025-05-25

React 中的 8 种可能导致应用崩溃的做法

在Medium上找到我

编辑:本文的第2点已修改,从读者角度来看更容易理解(也更令人毛骨悚然)。感谢 dev.to 上那位给我发邮件,解释之前困惑的用户!

我们很多人爱上 React 库的原因有很多。它可以让你轻松创建复杂的交互式用户界面。最棒的是,它能够直接在组件之上组合组件,而不会破坏其他组合组件的功能。

令人惊讶的是,就连FacebookInstagramPinterest等社交媒体巨头也大量使用它们,同时利用Google Maps等庞大的 API 创造无缝的用户体验

如果您目前正在使用 React 构建应用程序,或者考虑在未来的项目中使用 React,那么本教程非常适合您。我希望本教程能够通过展示一些值得您深思熟虑的代码实现,帮助您构建出色的 React 应用程序。

不用多说,以下是 React 中可能导致你的应用崩溃的 8 种做法:

1. 声明默认参数为空

我在之前的一篇文章中提到过这个话题,但这是一个令人毛骨悚然的“陷阱”,可能会在阴郁的星期五愚弄粗心的开发人员!毕竟,应用程序崩溃可不是闹着玩的——任何类型的崩溃,如果处理不当,都可能随时导致资金损失。

我曾经花了大量时间调试类似这样的问题:

const SomeComponent = ({ items = [], todaysDate, tomorrowsDate }) => {
  const [someState, setSomeState] = useState(null)

  return (
    <div>
      <h2>Today is {todaysDate}</h2>
      <small>And tomorrow is {tomorrowsDate}</small>
      <hr />
      {items.map((item, index) => (
        <span key={`item_${index}`}>{item.email}</span>
      ))}
    </div>
  )
}

const App = ({ dates, ...otherProps }) => {
  let items
  if (dates) {
    items = dates ? dates.map((d) => new Date(d).toLocaleDateString()) : null
  }

  return (
    <div>
      <SomeComponent {...otherProps} items={items} />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

在我们的 App 组件中,如果日期最终为 false,它将被初始化为 null。

如果你和我一样,我们的直觉告诉我们,如果 items 的值是 falsey,那么它应该默认初始化为空数组。但是,当 dates 为 falsey 时,我们的应用就会崩溃,因为 items 为 null。这是什么情况

如果没有传递值或未定义,则默认函数参数允许使用默认值初始化命名参数!

在我们的例子中,尽管 null 是假的,但它仍然是一个值!

所以下次将默认值设置为null时,请务必三思。如果预期值的类型是空数组,则可以直接将其初始化为空数组。

2. 使用方括号获取属性

有时,获取属性的方式可能会影响应用程序的行为。如果您想知道这种行为是什么,那就是应用程序崩溃了。以下是使用方括号执行对象查找的示例:

const someFunction = function() {
  const store = {
    people: {
      joe: {
        age: 16,
        gender: 'boy',
      },
      bob: {
        age: 14,
        gender: 'transgender',
      }
    }
  }
  return {
    getPersonsProfile(name) {
      return store.people[name]
    },
    foods: ['apple', 'pineapple'],
  }
}

const obj = someFunction()
const joesProfile = obj.getPersonsProfile('joe')

console.log(joesProfile)
/*
  result: 
      {
        age: 16,
        gender: boy,
      }
*/
Enter fullscreen mode Exit fullscreen mode

这些实际上是 100% 有效的用例,除了比对象键查找慢之外,它们并没有什么问题

无论如何,当出现无意的问题(例如一个小小的打字错误)时,真正的问题就开始在你的应用中显现:

const someFunction = function () {
  const store = {
    people: {
      joe: {
        age: 16,
        gender: 'boy',
      },
      bob: {
        age: 14, 
      gender: 'transgender',
      }
    }
  }
  return {
    getPersonsProfile(name) {
      return store.people[name]
    },
    foods: ['apple', 'pineapple'],
  }
}

const obj = someFunction()
const joesProfile = obj.getPersonsProfile('Joe')
const joesAge = joesProfile.age

console.log(joesAge)
Enter fullscreen mode Exit fullscreen mode

如果您或您的某个队友在对此代码片段进行一些增强时犯了一个小错误(例如将joe中的J大写),结果将立即返回undefined,并且会发生崩溃:

"TypeError: Cannot read property 'age' of undefined
    at tibeweragi.js:24:29
    at https://static.jsbin.com/js/prod/runner-4.1.7.min.js:1:13924
    at https://static.jsbin.com/js/prod/runner-4.1.7.min.js:1:10866"
Enter fullscreen mode Exit fullscreen mode

令人毛骨悚然的是,直到您的部分代码尝试使用该未定义的值进行属性查找时,应用程序才会崩溃!

因此,与此同时,joes 的个人资料(伪装的未定义)将在您的应用程序中传递,并且没有人能够知道这个隐藏的错误正在蔓延,直到一段代码执行某些属性查找,例如joesProfile.age,因为joesProfileundefined

为了避免崩溃,一些开发人员会这样做:如果查找最终不成功,则初始化一些默认的有效返回值:

const someFunction = function () {
  const store = {
    people: {
      joe: {
        age: 16,
        gender: 'boy',
      },
      bob: {
        age: 14, 
      gender: 'transgender',
      }
    }
  }
  return {
    getPersonsProfile(name) {
      return store.people[name] || {}
    },
    foods: ['apple', 'pineapple'],
  }
}
Enter fullscreen mode Exit fullscreen mode

至少现在应用程序不会崩溃了。这个故事的寓意是,当你使用方括号进行查找时,一定要处理无效的查找情况

对某些人来说,如果没有真实的例子,可能很难解释这种做法的严重性。所以我将举一个真实的例子。我将要展示的代码示例取自一个 8 个月前的仓库。为了保护此代码的隐私,我重命名了几乎所有变量,但代码设计、语法和架构保持不变

import { createSelector } from 'reselect'

// supports passing in the whole obj or just the string to correct the video type
const fixVideoTypeNaming = (videoType) => {
  let video = videoType

  // If video is a video object
  if (video && typeof video === 'object') {
    const media = { ...video }
    video = media.videoType
  }

  // If video is the actual videoType string
  if (typeof video === 'string') {
    // fix the typo because brian is an idiot
    if (video === 'mp3') {
      video = 'mp4'
    }
  }

  return video
}

/* -------------------------------------------------------
  ---- Pre-selectors
-------------------------------------------------------- */

export const getOverallSelector = (state) =>
  state.app[fixVideoTypeNaming(state.app.media.video.videoType)].options.total
    .overall

export const getSpecificWeekSelector = (state, props) =>
  state.app[fixVideoTypeNaming(state.app.media.video.videoType)].options.weekly[
    props.date
  ]

/* -------------------------------------------------------
  ---- Selectors
-------------------------------------------------------- */

export const getWeeklyCycleSelector = createSelector(
  getSpecificWeekSelector,
  (weekCycle) => weekCycle || null,
)

export const getFetchingTotalStatusSelector = createSelector(
  (state) =>
    state.app[fixVideoTypeNaming(state.app.media.video.videoType)].options.total
      .fetching,
  (fetching) => fetching,
)

export const getFetchErrorSelector = createSelector(
  (state) =>
    state.app[fixVideoTypeNaming(state.app.media.video.videoType)].options.total
      .fetchError,
  (fetchError) => fetchError,
)
Enter fullscreen mode Exit fullscreen mode

fixVideoTypeNaming函数会根据传入的参数值提取视频类型。如果参数是视频对象,它会从.videoType属性中提取视频类型。如果参数是字符串,则调用者会传入 videoType 属性,这样我们就可以跳过第一步。有人发现应用中多个地方的videoType .mp4属性拼写错误。为了快速临时解决这个问题,我们用fixVideoTypeNaming来修复这个拼写错误。

现在,有些人可能已经猜到了,该应用程序是使用redux构建的(因此有这样的语法)。

要使用这些选择器,您需要导入它们以在连接高阶组件中使用,以附加组件来监听该状态片段。

const withTotalCount = (WrappedComponent) => {
  class WithTotalCountContainer extends React.Component {
    componentDidMount = () => {
      const { total, dispatch } = this.props
      if (total == null) {
        dispatch(fetchTotalVideoTypeCount())
      }
    }

    render() {
      return <WrappedComponent {...this.props} />
    }
  }

  WithTotalCountContainer.propTypes = {
    fetching: PropTypes.bool.isRequired,
    total: PropTypes.number,
    fetchError: PropTypes.object,
    dispatch: PropTypes.func.isRequired,
  }

  WithTotalCountContainer.displayName = `withTotalCount(${getDisplayName(
    WrappedComponent,
  )})`

  return connect((state) => {
    const videoType = fixVideoTypeNaming(state.app.media.video.videoType)
    const { fetching, total, fetchError } = state.app.media.video[
      videoType
    ].options.total

    return { fetching, total, fetchError }
  })(WithTotalCountContainer)
}
Enter fullscreen mode Exit fullscreen mode

UI 组件:

const TotalVideoCount = ({ classes, total, fetching, fetchError }) => {
  if (fetching) return <LoadingSpinner />
  const hasResults = !!total
  const noResults = fetched && !total
  const errorOccurred = !!fetchError

  return (
    <Typography
      variant="h3"
      className={classes.root}
      error={!!fetched && !!fetchError}
      primary={hasResults}
      soft={noResults || errorOccurred}
      center
    >
      {noResults && 'No Results'}
      {hasResults && `$${formatTotal(total)}`}
      {errorOccurred && 'An error occurred.'}
    </Typography>
  )
}
Enter fullscreen mode Exit fullscreen mode

组件接收 HOC 传递给它的所有 props,并根据 props 传入的数据,根据条件显示信息。在理想情况下,这应该没问题。在非理想情况下,这暂时没问题。

如果我们回到容器并查看选择器选择其值的方式,我们实际上可能已经埋下了一颗定时炸弹,等待着一个开放的机会来攻击:

export const getOverallSelector = (state) =>
  state.app[fixVideoTypeNaming(state.app.media.video.videoType)].options.total
    .overall

export const getSpecificWeekSelector = (state, props) =>
  state.app[fixVideoTypeNaming(state.app.media.video.videoType)].options.weekly[
    props.date
  ]
Enter fullscreen mode Exit fullscreen mode

在开发任何类型的应用程序时,为了确保更高的信心水平并减少开发流程中的错误,常见的做法是在开发过程中实施测试,以确保应用程序按预期运行。

然而,对于这些代码片段,如果它们没有经过测试,并且没有及早处理,应用程序将来就会崩溃。

首先,state.app.media.video.videoType在链中深度有四层。如果另一位开发者在被要求修复应用程序的其他部分时不小心犯了错误,导致state.app.media.video变为undefined ,该怎么办?应用程序会崩溃,因为它无法读取undefined 的 videoType属性。

此外,如果视频类型 (videoType)又出现了拼写错误,而fixVideoTypeNaming又没有及时更新以解决mp3问题,那么应用可能会再次面临意外崩溃的风险,除非真用户遇到这个问题,否则根本无法察觉。而到那时,就为时已晚了

假设应用永远不会遇到此类错误绝对不是什么好习惯。请务必小心!

3.渲染时粗心地检查空对象

很久以前,在条件渲染组件的黄金时代,我曾经做过一件事:使用 来检查对象中是否已经填充了数据Object.keys。如果有数据,则在条件满足的情况下,组件将继续渲染:

const SomeComponent = ({ children, items = {}, isVisible }) => (
  <div>
    {Object.keys(items).length ? (
      <DataTable items={items} />
    ) : (
      <h2>Data has not been received</h2>
    )}
  </div>
)
Enter fullscreen mode Exit fullscreen mode

假设我们调用了某个 API,并在响应中收到了一个对象类型的items 。话虽如此,乍一看这似乎完全没问题。items预期类型是对象,因此使用Object.keys完全没问题。毕竟,我们确实将 items 初始化为一个空对象,以防万一出现 bug 导致其变成 false 值。

但我们不应该相信服务器总是返回相同的结构。如果 items 将来变成数组怎么办?它Object.keys(items)不会崩溃,但会返回像 这样的奇怪输出["0", "1", "2"]。你认为使用该数据渲染的组件会如何反应?

但这还不是最糟糕的部分。代码片段中最糟糕的部分是,如果items在 props 中被接收为null值,那么它items 甚至不会初始化为你提供的默认值

然后你的应用程序就会在开始做其他事情之前崩溃

"TypeError: Cannot convert undefined or null to object
    at Function.keys (<anonymous>)
    at yazeyafabu.js:4:45
    at https://static.jsbin.com/js/prod/runner-4.1.7.min.js:1:13924
    at https://static.jsbin.com/js/prod/runner-4.1.7.min.js:1:10866"
Enter fullscreen mode Exit fullscreen mode

再次强调,请大家小心!

4.渲染前粗心地检查数组是否存在

这可能与#3的情况非常相似,但是数组和对象经常互换使用,因此它们值得拥有自己的部分。

如果你有这样做的习惯:

render() {
  const { arr } = this.props
  return (
    <div>
      {arr && arr.map()...}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

然后,确保至少进行过单元测试,以便始终关注该代码arr,或者在将其传递给 render 方法之前尽早正确处理,否则,如果该值arr变成对象字面量,应用程序就会崩溃。当然,&&运算符会将其视为真值,并尝试对该对象字面量进行.map操作,最终导致整个应用程序崩溃。

所以请记住这一点。把你的精力和挫败感留给那些值得你特别关注的更大问题吧!;)

5. 不使用 Linter

如果您在开发应用程序时没有使用任何类型的 linter,或者您根本不知道它们是什么,请允许我详细说明一下它们在开发中为什么有用。

我用来协助我开发流程的 linter 是ESLint,这是一个非常知名的 JavaScript linting 工具,它允许开发人员在不执行代码的情况下发现代码中的问题。

这个工具非常有用,它可以充当你的“半导师”,实时纠正你的错误,就像有人在指导你一样。它甚至会解释你的代码可能存在的问题,并建议你用什么来替代它们!

以下是一个例子:

eslint

eslint 最酷的地方在于,如果你不喜欢某些规则,或者不认同其中某些规则,你可以简单地禁用它们,这样它们就不会再在开发过程中显示为 linting 警告/错误了。随便你怎么想,对吧?

6. 渲染列表时解构

我以前见过好几个人遇到这种情况,而且这个 bug 很难发现。基本上,当你有一个项目列表,并且要为列表中的每个项目渲染一堆组件时,你的应用可能会出现一个 bug:如果将来某个时候列表中的某个项目的值不符合你的预期,而你的应用不知道如何处理这个值类型,它可能会崩溃。

以下是一个例子:

const api = {
  async getTotalFrogs() {
    return {
      data: {
        result: [
          { name: 'bob the frog', tongueWidth: 50, weight: 8 },
          { name: 'joe the other frog', tongueWidth: 40, weight: 5 },
          { name: 'kelly the last frog', tongueWidth: 20, weight: 2 },
        ],
      },
    }
  },
}

const getData = async ({ withTongues = false }) => {
  try {
    const response = await api.getTotalFrogs({ withTongues })
    return response.data.result
  } catch (err) {
    throw err
  }
}

const DataList = (props) => {
  const [items, setItems] = useState([])
  const [error, setError] = useState(null)

  React.useEffect(() => {
    getData({ withTongues: true })
      .then(setItems)
      .catch(setError)
  }, [])

  return (
    <div>
      {Array.isArray(items) && (
        <Header size="tiny" inverted>
          {items.map(({ name, tongueWidth, weight }) => (
            <div style={{ margin: '25px 0' }}>
              <div>Name: {name}</div>
              <div>Width of their tongue: {tongueWidth}cm</div>
              <div>Weight: {weight}lbs</div>
            </div>
          ))}
        </Header>
      )}
      {error && <Header>You received an error. Do you need a linter?</Header>}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

青蛙1

这段代码完全没问题。现在我们看一下 api 调用,返回的内容不是这个:

const api = {
  async getTotalFrogs() {
    return {
      data: {
        result: [
          { name: 'bob the frog', tongueWidth: 50, weight: 8 },
          { name: 'joe the other frog', tongueWidth: 40, weight: 5 },
          { name: 'kelly the last frog', tongueWidth: 20, weight: 2 },
        ],
      },
    }
  },
}
Enter fullscreen mode Exit fullscreen mode

如果当 api 客户端中出现意外情况并返回此数组时,数据流的处理方式存在问题,该怎么办?

const api = {
  async getTotalFrogs() {
    return {
      data: {
        result: [
          { name: 'bob the frog', tongueWidth: 50, weight: 8 },
          undefined,
          { name: 'kelly the last frog', tongueWidth: 20, weight: 2 },
        ],
      },
    }
  },
}
Enter fullscreen mode Exit fullscreen mode

您的应用程序将会崩溃,因为它不知道如何处理该问题:

Uncaught TypeError: Cannot read property 'name' of undefined
    at eval (DataList.js? [sm]:65)
    at Array.map (<anonymous>)
    at DataList (DataList.js? [sm]:64)
    at renderWithHooks (react-dom.development.js:12938)
    at updateFunctionComponent (react-dom.development.js:14627)
Enter fullscreen mode Exit fullscreen mode

因此,为了防止应用程序崩溃,您可以在每次迭代时设置一个默认对象:

{
  items.map(({ name, tongueWidth, weight } = {}) => (
    <div style={{ margin: '25px 0' }}>
      <div>Name: {name}</div>
      <div>Width of their tongue: {tongueWidth}cm</div>
      <div>Weight: {weight}lbs</div>
    </div>
  ))
}
Enter fullscreen mode Exit fullscreen mode

现在,当您的用户没有看到页面崩溃时,他们就不必对您的技术和专业知识做出判断:

青蛙2

然而,即使应用程序不再崩溃,我还是建议进一步处理缺失值,例如对具有类似问题的整个项目返回 null,因为它们中没有任何数据。

7. 对要实施的项目研究不足

我过去犯的一个严重错误是对自己实施的搜索输入过于自信,过早地相信自己的意见。

我这是什么意思呢?嗯,我之前不太自信的不是搜索输入组件。这个组件应该很容易实现……而事实也确实如此。

整个搜索功能出现的问题的真正罪魁祸首是查询中包含的字符

当我们将关键字作为查询发送到搜索 API 时,仅仅认为用户输入的每个键都是有效的并不总是足够的,即使它们在键盘上。

只要 100% 确保这样的正则表达式能够按预期工作,并避免遗漏任何可能导致应用程序崩溃的无效字符:

const hasInvalidChars = /^.*?(?=[\+\^#%&$\*:<>\?/\{\|\}\[\]\\\)\(]).*$/g.test(
  inputValue,
)
Enter fullscreen mode Exit fullscreen mode

该示例是搜索 API 的最新、已建立的正则表达式。

之前的情况如下:

const hasInvalidChars = /^.*?(?=[\+\^#%&$\*:<>\?/\{\|\}\[\]\)\(]).*$/g.test(
  inputValue,
)

const callApi = async (keywords) => {
  try {
    const url = `https://someapi.com/v1/search/?keywords=${keywords}/`
    return api.searchStuff(url)
  } catch (error) {
    throw error
  }
}
Enter fullscreen mode Exit fullscreen mode

正如您所看到的,斜线/丢失了,这导致应用程序崩溃!如果该字符最终通过网络发送到 API,猜猜 API 认为 URL 是什么?

另外,我不会 100% 相信你在网上找到的例子。很多例子都没有经过充分测试,而且正则表达式在大多数用例中并没有一个真正的标准。

7.不限制文件输入的大小

限制用户选择的文件大小是一种很好的做法,因为大多数情况下,当文件可以通过某种方式压缩而不会出现任何明显的质量下降迹象时,您实际上并不需要非常大的文件。

但还有一个更重要的原因,说明为什么将尺寸限制在一定范围内是一个好习惯。在我的公司,我们注意到过去用户在上传图片偶尔会“卡住” 。并非每个人都拥有 Alienware 17 R5,因此您必须考虑用户的特定情况。

以下是将文件大小限制为 5 MB(5,000,000 字节)的示例:

import React, { useState, useEffect } from 'react'

const useUploadStuff = () => {
  const [files, setFiles] = useState([])

  // Limit the file sizes here
  const onChange = (e) => {
    const arrFiles = Array.from(e.target.files)
    const filesUnder5mb = arrFiles.filter((file) => {
      const bytesLimit = 5000000
      if (file.size > bytesLimit) {
        // optionally process some UX about this file size
      }
      return file.size < bytesLimit
    })
    setFiles(filesUnder5mb)
  }

  useEffect(() => {
    if (files.length) {
      // do something with files
    }
  }, [files])

  return {
    files,
    onChange,
  }
}

const UploadStuff = () => {
  const { onChange } = useUploadStuff()

  return (
    <div>
      <h2 style={{ color: '#fff' }}>Hi</h2>
      <div>
        <input
          style={{ color: '#fff' }}
          onChange={onChange}
          type="file"
          placeholder="Upload Stuff"
          multiple
        />
      </div>
    </div>
  )
}

export default UploadStuff
Enter fullscreen mode Exit fullscreen mode

您肯定不希望用户在应该上传文档时却上传视频游戏!

结论

这篇文章到此结束!

将会有第二部分,因为我只完成了列表的一半(哎呀!)

无论如何,感谢您的阅读,也请关注我,获取后续更新!祝您7月4日快乐!

在Medium上找到我

文章来源:https://dev.to/jsmanifest/8-practices-in-react-that-will-crash-your-app-in-the-future-2le5
PREV
你应该知道的 8 个 React 应用实用实践
NEXT
增强 React 应用的 8 种神奇方法