构建 React 应用程序时不要做的 10 件事

2025-05-28

构建 React 应用程序时不要做的 10 件事

如果你还没有关注我的话,请关注:)

React是一种非常流行的 Web 开发工具,我相信 React 的粉丝们一定非常幸运,能够使用如此优秀的库:)

不幸的是,生活中没有什么是完美的,反应也不例外。

React 有其自身的一系列陷阱——如果您现在不处理它们,其中一些可能会对您的应用程序造成严重问题

以下是构建 React 应用程序时不要做的 10 件事:

1. 花费太多时间在自己的私人世界里

反应隔离

如果你在项目中花费太多时间编写代码,而没有花时间了解社区中发生的事情,那么你可能会犯下社区中已经报告过的不良代码。而且,你可能会继续犯下这些不良代码,直到你重复了20次,才终于有机会在Medium上看到这些代码被举报

当这种情况发生时,您现在必须回过头来重构这 20 个代码实现,因为您发现得太晚了,而其他人已经走在您前面并开始了解更新的消息。

当 React 发布Hooks时,我兴奋不已,开始构建一堆小项目来体验这些让所有人都兴奋不已的新玩意儿。在读到一些资料说 Hooks 即将稳定发布后,我开始更认真地在我的项目中实现它们。我像个老大一样到处使用useStateuseEffect

然后我偶然发现有人链接到这个推特推文,这促使我对 useReducer 做了更多的研究。

30 分钟的研究足以让我回过头来重构大量代码。

2. 使用.bind(不是类组件构造函数)

我想大多数 React 开发者都知道,如果我们想在类方法中引用this来访问类实例,就应该使用.bind 来绑定类方法。(除非你使用转译器来转译类的属性和方法。)

这很棒,我同意更喜欢用箭头函数来声明它们。

但这部分我主要讨论的不是这个,而是内联函数——或者说,在 React 组件的render 方法中定义,并作为 prop 传递给子组件的函数。

当在 render 方法中定义内联函数时,React 每次组件重新渲染时都会指定一个新的函数实例。这会导致性能问题,因为重新渲染会产生浪费

我们来看这个例子:

const ShowMeTheMoney = () => {
  const [money, setMoney] = useState(0)

  const showThemTheMoney = (money) => {
    setMoney(money)
  }

  const hideTheMoney = () => {
    setMoney(null)
  }

  const sayWhereTheMoneyIs = (msg) => {
    console.log(msg)
  }

  return (
    <div>
      <h4>Where is the money?</h4>
      <hr />
      <div style={{ display: 'flex', alignItems: 'center' }}>
        <SomeCustomButton
          type="button"
          onClick={() => sayWhereTheMoneyIs("I don't know")}
        >
          I'll tell you
        </SomeCustomButton>{' '}
        <SomeCustomButton type="button" onClick={() => showThemTheMoney(0.05)}>
          I'll show you
        </SomeCustomButton>
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

我们知道onClick={() => sayWhereTheMoneyIs("I don't know")}是内onClick={() => showThemTheMoney(0.05)}函数

我见过一些教程(包括来自Udemy的一个),鼓励这样做:

return (
  <div>
    <h4>Where is the money?</h4>
    <hr />
    <div style={{ display: 'flex', alignItems: 'center' }}>
      <SomeCustomButton
        type="button"
        onClick={sayWhereTheMoneyIs.bind(null, "I don't know")}
      >
        I'll tell you
      </SomeCustomButton>{' '}
      <SomeCustomButton
        type="button"
        onClick={showThemTheMoney.bind(null, 0.05)}
      >
        I'll show you
      </SomeCustomButton>
    </div>
  </div>
)
Enter fullscreen mode Exit fullscreen mode

似乎缓存了引用,从而避免了不必要的重新渲染,因为它们没有在渲染方法中使用箭头内联函数,但实际上它们仍然在每个渲染阶段创建新函数!

如果我们在类组件流行的时候一直关注 React 生态系统中的社区,那么我们中的一些人可能已经知道这一点。

然而,自从 React Hooks 发布以来,关于 .bind 的讨论就逐渐淡化了,因为类组件已经不再那么流行了——通常,当谈到.bind时,通常都是关于绑定类方法的。更糟糕的是,上面这些例子根本没有绑定到类方法,所以如果你不够细心,就很难注意到这里的后果。

新手尤其应该注意这种反模式

3. 将动态值作为键传递给子级

您是否曾经遇到过被迫向被映射的子项提供唯一密钥的情况?

提供唯一的键是很好的:

const Cereal = ({ items, ...otherProps }) => {
  const indexHalf = Math.floor(items.length / 2)
  const items1 = items.slice(0, indexHalf)
  const items2 = items.slice(indexHalf)
  return (
    <>
      <ul>
        {items1.map(({ to, label }) => (
          <li key={to}>
            <Link to={to}>{label}</Link>
          </li>
        ))}
      </ul>
      <ul>
        {items2.map(({ to, label }) => (
          <li key={to}>
            <Link to={to}>{label}</Link>
          </li>
        ))}
      </ul>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

现在假设items1中的某些值恰好与items2中的某些值相同

我发现,当有些人想要重构类似的组件时,他们最终会做这样的事情:

import { generateRandomUniqueKey } from 'utils/generating'

const Cereal = ({ items, ...otherProps }) => {
  const indexHalf = Math.floor(items.length / 2)
  const items1 = items.slice(0, indexHalf)
  const items2 = items.slice(indexHalf)
  return (
    <>
      <ul>
        {items1.map(({ to, label }) => (
          <li key={generateRandomUniqueKey()}>
            <Link to={to}>{label}</Link>
          </li>
        ))}
      </ul>
      <ul>
        {items2.map(({ to, label }) => (
          <li key={generateRandomUniqueKey()}>
            <Link to={to}>{label}</Link>
          </li>
        ))}
      </ul>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

确实为每个子节点提供了唯一的密钥,完成了任务。但是有两个问题:

  1. 我们不仅让 React 在生成唯一值方面做了不必要的工作,而且由于每次的键都不同,我们最终还会在每次渲染时重新创建所有节点

  2. React 中的关键概念在于身份。为了识别哪个组件是哪个组件,键确实需要唯一,但并非如此

类似这样的情况会变得更好一些:

import { generateRandomUniqueKey } from 'utils/generating'

const Cereal = ({ items, ...otherProps }) => {
  const indexHalf = Math.floor(items.length / 2)
  const items1 = items.slice(0, indexHalf)
  const items2 = items.slice(indexHalf)
  return (
    <>
      <ul>
        {items1.map(({ to, label }) => (
          <li key={`items1_${to}`}>
            <Link to={to}>{label}</Link>
          </li>
        ))}
      </ul>
      <ul>
        {items2.map(({ to, label }) => (
          <li key={`items2_${to}`}>
            <Link to={to}>{label}</Link>
          </li>
        ))}
      </ul>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

现在我们应该确信每个项目在保留其身份的同时都会拥有自己独特的键值。

4. 声明默认参数为空

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

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组件中,如果日期最终为falsey,它将被初始化为null

如果我们运行代码——如果你和我一样,我们的直觉告诉我们,如果 items 的值是 falsey,那么默认情况下应该将其初始化为空数组。但是,当datesfalsey 时,我们的应用程序会崩溃,因为items为 null。这是什么情况?

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

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

这个错误让我花了不少时间去调试,尤其是当null值来自 redux reducer 的时候!唉。

5. 保留重复代码

当您急于推出修复程序时,复制和粘贴代码可能很诱人,因为有时这可能是最快的解决方案。

以下是重复代码的示例:

const SomeComponent = () => (
  <Body noBottom>
    <Header center>Title</Header>
    <Divider />
    <Background grey>
      <Section height={500}>
        <Grid spacing={16} container>
          <Grid xs={12} sm={6} item>
            <div className={classes.groupsHeader}>
              <Header center>Groups</Header>
            </div>
          </Grid>
          <Grid xs={12} sm={6} item>
            <div>
              <img src={photos.groups} alt="" className={classes.img} />
            </div>
          </Grid>
        </Grid>
      </Section>
    </Background>
    <Background grey>
      <Section height={500}>
        <Grid spacing={16} container>
          <Grid xs={12} sm={6} item>
            <div className={classes.labsHeader}>
              <Header center>Labs</Header>
            </div>
          </Grid>
          <Grid xs={12} sm={6} item>
            <div>
              <img src={photos.labs} alt="" className={classes.img} />
            </div>
          </Grid>
        </Grid>
      </Section>
    </Background>
    <Background grey>
      <Section height={300}>
        <Grid spacing={16} container>
          <Grid xs={12} sm={6} item>
            <div className={classes.partnersHeader}>
              <Header center>Partners</Header>
            </div>
          </Grid>
          <Grid xs={12} sm={6} item>
            <div>
              <img src={photos.partners} alt="" className={classes.img} />
            </div>
          </Grid>
        </Grid>
      </Section>
    </Background>
  </Body>
)
Enter fullscreen mode Exit fullscreen mode

现在是时候开始思考如何抽象这些组件,使它们能够在不改变实现的情况下多次重用。如果某个Grid组件相对于其周围的 *Grid 容器*存在样式问题,你就必须手动更改每一个组件。

更好的编码方式可能是抽象出重复的部分,并传递略有不同的道具:

const SectionContainer = ({
  bgProps,
  height = 500,
  header,
  headerProps,
  imgProps,
}) => (
  <Background {...bgProps}>
    <Section height={height}>
      <Grid spacing={16} container>
        <Grid xs={12} sm={6} item>
          <div {...headerProps}>
            <Header center>{header}</Header>
          </div>
        </Grid>
        <Grid xs={12} sm={6} item>
          <div>
            <img {...imgProps} />
          </div>
        </Grid>
      </Grid>
    </Section>
  </Background>
)

const SomeComponent = () => (
  <Body noBottom>
    <Header center>Title</Header>
    <Divider />
    <SectionContainer
      header="Groups"
      headerProps={{ className: classes.groupsHeader }}
      imgProps={{ src: photos.groups, className: classes.img }}
    />
    <SectionContainer
      bgProps={{ grey: true }}
      header="Labs"
      headerProps={{ className: classes.labsHeader }}
      imgProps={{ src: photos.labs, className: classes.img }}
    />
    <SectionContainer
      height={300}
      header="Partners"
      headerProps={{ className: classes.partnersHeader }}
      imgProps={{ src: photos.partners, className: classes.img }}
    />
  </Body>
)
Enter fullscreen mode Exit fullscreen mode

因此,现在如果你的老板最终改变主意并想将所有这些部分的高度设为约300px,你只有一个地方可以更改它。

现在,如果我们想要创建一个支持多种用例的组件,我并不是想推荐这样的解决方案,而是针对特定用途,我们知道它只会在特定环境中被复用。一个支持多种用例的、更动态、可复用的SectionContainer解决方案,很可能应该被编码得更通用,就像这样,而且仍然不需要改变实现:

const SectionContainer = ({
  bgProps,
  sectionProps,
  children,
  gridContainerProps,
  gridColumnLeftProps,
  gridColumnRightProps,
  columnLeft,
  columnRight,
}) => (
  <Background {...bgProps}>
    <Section {...sectionProps}>
      {children || (
        <Grid spacing={16} container {...gridContainerProps}>
          <Grid xs={12} sm={6} item {...gridColumnLeftProps}>
            {columnLeft}
          </Grid>
          <Grid xs={12} sm={6} item {...gridColumnRightProps}>
            {columnRight}
          </Grid>
        </Grid>
      )}
    </Section>
  </Background>
)
Enter fullscreen mode Exit fullscreen mode

这样,我们现在允许开发人员根据需要选择性地扩展组件的任何部分,同时保留底层实现。

6. 在构造函数中初始化 Props

在构造函数中初始化状态时:

import React from 'react'

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      items: props.items,
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

你可能会遇到 bug。这是因为构造函数只被调用一次,也就是组件第一次创建时。

下次您尝试更改 props 时,状态将保留其先前的值,因为构造函数不会在重新渲染时被调用。

如果您还没有遇到这个问题,我希望这对您有所帮助!

如果你想知道如何让 props 与 state 同步,更好的方法是这样的:

import React from 'react'

class App extends React.Component {
  constructor(props) {
    super(props)
    // Initialize the state on mount
    this.state = {
      items: props.items,
    }
  }

  // Keep the state in sync with props in further updates
  componentDidUpdate = (prevProps) => {
    const items = []
    // after  calculations comparing prevProps with this.props
    if (...) {
      this.setState({ items })
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

7. 使用&&进行条件渲染

有条件地渲染组件时的一个常见问题是使用 && 运算符。

如果条件不满足,React 会尝试渲染你提供的任何内容作为替代输出。因此,当我们看这个:

const App = ({ items = [] }) => (
  <div>
    <h2>Here are your items:</h2>
    <div>
      {items.length &&
        items.map((item) => <div key={item.label}>{item.label}</div>)}
    </div>
  </div>
)
Enter fullscreen mode Exit fullscreen mode

当items.length为空时,屏幕上实际上会渲染数字0。JavaScript将数字0视为假值,因此当items为空数组时,&&运算符不会对其右侧的表达式进行求值,而只会返回第一个值。

如果我想保留语法,我通常会使用双重否定:

const App = ({ items = [] }) => (
  <div>
    <h2>Here are your items:</h2>
    <div>
      {!!items.length &&
        items.map((item) => <div key={item.label}>{item.label}</div>)}
    </div>
  </div>
)
Enter fullscreen mode Exit fullscreen mode

这样,如果items是一个空数组,并且评估的输出是一个布尔值,则 react 将不会在屏幕上呈现任何内容。

8. 不传播先前的状态

偶尔会出现一些由于不小心实现状态更新逻辑而导致的错误。

最近遇到的情况与 React Hooks 有关,具体来说就是useReducer 的实现。以下是这个问题的一个简单示例:

const something = (state) => {
  let newState = { ...state }
  const indexPanda = newState.items.indexOf('panda')
  if (indexPanda !== -1) {
    newState.items.splice(indexPanda, 1)
  }
  return newState
}

const initialState = {
  items: [],
}

const reducer = (state, action) => {
  switch (action.type) {
    case 'add-item':
      return { ...state, items: [...something(state).items, action.item] }
    case 'clear':
      return { ...initialState }
    default:
      return state
  }
}
Enter fullscreen mode Exit fullscreen mode

something函数调用并复制状态时,底层的items属性并未改变。当我们使用.splice对其进行变异时,这会改变state.items 属性,从而引入错误。

在大型代码中尤其要注意这一点。我们可能都经历过像上面这样的小例子,但当事情变得混乱时,这一点必须时刻牢记,因为它很容易被忘记,尤其是在你面临将代码交付生产的压力时!

9. 没有明确地将 Props 传递给子组件

通常建议在传递给子组件的 props 中明确说明。

这样做有几个很好的理由:

  1. 更轻松的调试体验
    1. 作为开发人员,您知道传递给每个孩子的内容。
      1. 其他开发人员也会知道这一点,并且可以更轻松地阅读代码
  2. 更容易理解组件的功能
    1. 明确传递 props 的另一个好处是,当你这样做时,它还能以一种每个人都能理解的方式记录你的代码,甚至不需要正式的文档。这节省了时间
  3. 为了确定组件是否应该重新渲染,需要更少的道具。

尽管可以有一些非常巧妙的用例来传播所有的道具。

例如,如果父组件在将 props 传递给子组件之前需要一两件东西,那么他们(和你)可以轻松地这样做:

const Parent = (props) => {
  if (props.user && props.user.email) {
    // Fire some redux action to update something globally that another
    //    component might need to know about
  }

  // Continue on with the app
  return <Child {...props} />
}
Enter fullscreen mode Exit fullscreen mode

只要确保你不会陷入这样的境地:

<ModalComponent
  open={aFormIsOpened}
  onClose={() => closeModal(formName)}
  arial-labelledby={`${formName}-modal`}
  arial-describedby={`${formName}-modal`}
  classes={{
    root: cx(classes.modal, { [classes.dialog]: shouldUseDialog }),
    ...additionalDialogClasses,
  }}
  disableAutoFocus
>
  <div>
    {!dialog.opened && (
      <ModalFormRoot
        animieId={animieId}
        alreadySubmitted={alreadySubmitted}
        academy={academy}
        user={user}
        clearSignature={clearSignature}
        closeModal={closeModal}
        closeImageViewer={closeImageViewer}
        dialog={dialog}
        fetchAcademyMember={fetchAcademyMember}
        formName={formName}
        formId={formId}
        getCurrentValues={getCurrentValues}
        header={header}
        hideActions={formName === 'signup'}
        hideClear={formName === 'review'}
        movieId={movie}
        tvId={tvId}
        openPdfViewer={openPdfViewer}
        onSubmit={onSubmit}
        onTogglerClick={onToggle}
        seniorMember={seniorMember}
        seniorMemberId={seniorMemberId}
        pdfViewer={pdfViewer}
        screenViewRef={screenViewRef}
        screenRef={screenRef}
        screenInputRef={screenInputRef}
        updateSignupFormValues={updateSignupFormValues}
        updateSigninFormValues={updateSigninFormValues}
        updateCommentFormValues={updateCommentFormValues}
        updateReplyFormValues={updateReplyFormValues}
        validateFormId={validateFormId}
        waitingForPreviousForm={waitingForPreviousForm}
        initialValues={getCurrentValues(formName)}
        uploadStatus={uploadStatus}
        uploadError={uploadError}
        setUploadError={setUploadError}
        filterRolesFalseys={filterRolesFalseys}
      />
    )}
  </div>
</ModalComponent>
Enter fullscreen mode Exit fullscreen mode

如果您这样做,请考虑将组件部分拆分为单独的组件,以便更清晰、更可定制。

10. 支柱钻井

将 props 传递给多个子组件就是所谓的“代码异味”

如果您不知道什么是 prop 钻探,它的意思是当父级将 props 传递到树深处的多个级别的组件时。

现在的问题不在于父组件,也不在于子组件。它们的实现应该保持一致。中间的组件可能会成为 React 应用的问题。

这是因为现在中间的组件紧密耦合,暴露了太多它们根本不需要的信息。最糟糕的是,当父组件重新渲染时,中间的组件也会重新渲染,从而对链上所有子组件产生多米诺骨牌效应。

一个好的解决方案是使用context。或者,使用redux来处理 props(不过 props 最终还是会被序列化)。

结论

这篇文章到此结束 :) 我希望这篇文章对您有所帮助,并请务必关注我以获取以后的文章!

如果你还没有关注我的话,请关注:)

文章来源:https://dev.to/jsmanifest/10-things-not-to-do-when-building-react-applications-58a7
PREV
如何在 Google Play 商店发布 PWA
NEXT
使用 VuePress 和 Tailwind.css 构建漂亮的网站