在 React 应用中编写更简洁代码的 14 个实用技巧

2025-05-25

在 React 应用中编写更简洁代码的 14 个实用技巧

在Medium上找到我

编写简洁的代码在你的职业生涯中,尤其是在你试图获得第一份开发者工作时,是必须的。这本质上决定了你能否成为一名优秀的团队成员,并且可能决定你在面试中能否成功。你的代码编写方式是他们在做出招聘决定之前想要考察的因素之一。你的代码应该易于理解,而不仅仅是机器能够理解。

本文列出的这些内容对项目规模越大的项目越适用,但对于规模较小的项目则可能并非必要。请自行判断 :)

以下是在 React 应用中编写更简洁代码的 14 条有益技巧:

1. 解构你的道具

解构 props 是让代码更简洁、更易于维护的好方法。这是因为这样可以清晰地定义或声明某个对象(例如组件)的用途,并且开发人员无需费力阅读组件的实现来找出所有与组件相关的 props。

它还使您能够为它们声明默认值,您可能已经多次看到过:

import React from 'react'
import Button from 'components/Button'

const MyComponent = ({ placeholder = '', style, ...otherProps }) => {
  return (
    <Button
      type="button"
      style={{
        border: `1px solid ${placeholder ? 'salmon' : '#333'}`,
        ...style,
      }}
      {...otherProps}
    >
      Click Me
    </Button>
  )
}

export default MyComponent
Enter fullscreen mode Exit fullscreen mode

我发现 JavaScript 解构最酷的事情之一是它允许你支持不同形式的参数。

例如,如果您有一个身份验证函数,该函数曾经将token作为参数来验证用户身份,而现在由于新的服务器响应结构而希望将其作为参数jwt_token,则您可以轻松支持这两个参数,而无需更改太多代码:

// before refactoring
async function authenticate({ user_id, token }) {
  try {
    const response = await axios.post('https://someapi.com/v1/auth/', {
      user_id,
      token,
    })
    console.log(response)
    return response.data
  } catch (error) {
    console.error(error)
    throw error
  }
}

// after refactoring
async function authenticate({ user_id, jwt_token, token = jwt_token }) {
  try {
    const response = await axios.post('https://someapi.com/v1/auth/', {
      user_id,
      token,
    })
    console.log(response)
    return response.data
  } catch (error) {
    console.error(error)
    throw error
  }
}
Enter fullscreen mode Exit fullscreen mode

jwt_token当代码执行到 时, 会被求值token,所以如果jwt_token是有效令牌,且tokenundefined,那么 的值token就会变成 的值jwt_token。如果token已经是某个真值(一个真正的令牌),它就会保持原样。

2. 将组件文件夹化

让我们看一下下面的目录结构:

  • 源码
    • 成分
    • Breadcrumb.js
    • CollapsedSeparator.js
    • 输入
      • index.js
      • 输入.js
      • utils.js
      • focusManager.js
    • 卡片
      • index.js
      • Card.js
      • CardDivider.js
    • Button.js
    • Typography.js

众所周知,面包屑导航的核心功能之一就是与某种分隔符相关联。CollapsedSeparator组件 被导入到 内部Breadcrumb.js,因此我们知道它们在实现上是相关的。然而,不了解这些信息的人可能会认为BreadcrumbCollapsedSeparator是两个完全独立的组件,彼此之间毫无关联——尤其是在CollapsedSeparator没有任何明确迹象表明它与面包屑导航相关的情况下,例如带有前缀Breadcrumb(例如 BreadcrumbCollapsedSeparator.js)。

既然我们知道它们是相关的,我们可能会质疑为什么它们不像 Input 和 Card 那样放在同一个文件夹中,并开始做出一些奇怪的假设,比如“我想知道是否有人把它放在那里,看看我是否会像个好心人一样把它拿出来……”。而整洁代码实践的效果应该恰恰相反——开发人员应该能够快速阅读你的代码并了解情况!

将面包屑文件夹化看起来是这样的:

  • 源码
    • 成分
    • 面包屑
      • index.js
      • Breadcrumb.js
      • CollapsedSeparator.js
    • 输入
      • index.js
      • 输入.js
      • utils.js
      • focusManager.js
    • 卡片
      • index.js
      • Card.js
      • CardDivider.js
    • Button.js
    • Typography.js

现在,无论之后创建了多少个与Breadcrumb相关的组件,只要它们位于同一目录中,我们就会知道它们是相关的:Breadcrumb

  • 源码
    • 成分
    • 面包屑
      • index.js
      • Breadcrumb.js
      • CollapsedSeparator.js
      • Expander.js
      • BreadcrumbText.js
      • BreadcrumbHotdog.js
      • BreadcrumbFishes.js
      • BreadcrumbLeftOvers.js
      • BreadcrumbHead.js
      • BreadcrumbAddict.js
      • BreadcrumbDragon0814.js
      • BreadcrumbContext.js
    • 输入
      • index.js
      • 输入.js
      • utils.js
      • focusManager.js
    • 卡片
      • index.js
      • Card.js
      • CardDivider.js
    • Button.js
    • Typography.js
import React from 'react'
import Breadcrumb, {
  CollapsedSeparator,
  Expander,
  BreadcrumbText,
  BreadcrumbHotdog,
  BreadcrumbFishes,
  BreadcrumbLeftOvers,
  BreadcrumbHead,
  BreadcrumbAddict,
  BreadcrumbDragon0814,
} from '../../../../../../../../../../components/Breadcrumb'

const withBreadcrumbHotdog = (WrappedComponent) => (props) => (
  <WrappedComponent BreadcrumbHotdog={BreadcrumbHotdog} {...props} />
)

const WorldOfBreadcrumbs = ({
  BreadcrumbHotdog: BreadcrumbHotdogComponent,
}) => {
  const [hasFishes, setHasFishes] = React.useState(false)

  return (
    <BreadcrumbDragon0814
      hasFishes={hasFishes}
      render={(results) => (
        <BreadcrumbFishes>
          {({ breadcrumbFishes }) => (
            <BreadcrumbLeftOvers.Provider>
              <BreadcrumbHotdogComponent>
                <Expander>
                  <BreadcrumbText>
                    <BreadcrumbAddict>
                      <pre>
                        <code>{JSON.stringify(results, null, 2)}</code>
                      </pre>
                    </BreadcrumbAddict>
                  </BreadcrumbText>
                </Expander>
                {hasFishes
                  ? breadcrumbFishes.map((fish) => (
                      <>
                        {fish}
                        <CollapsedSeparator />
                      </>
                    ))
                  : null}
              </BreadcrumbHotdogComponent>
            </BreadcrumbLeftOvers.Provider>
          )}
        </BreadcrumbFishes>
      )}
    />
  )
}

export default withBreadcrumbHotdog(WorldOfBreadcrumbs)
Enter fullscreen mode Exit fullscreen mode

3. 使用标准命名约定命名组件

使用标准约定命名您的组件可以让其他开发人员更轻松地阅读您的代码。

例如,高阶组件通常会加上with大多数人习惯的前缀:

import React from 'react'
import hoistNonReactStatics from 'hoist-non-react-statics'
import getDisplayName from 'utils/getDisplayName'

const withFreeMoney = (WrappedComponent) => {
  class WithFreeMoney extends React.Component {
    giveFreeMoney() {
      return 50000
    }

    render() {
      return (
        <WrappedComponent
          additionalMoney={[
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
          ]}
          {...this.props}
        />
      )
    }
  }

  WithFreeMoney.displayName = `withFreeMoney(${getDisplayName(
    WrappedComponent,
  )}$)`
  hoistNonReactStatics(WithFreeMoney, WrappedComponent)

  return WithFreeMoney
}

export default withFreeMoney
Enter fullscreen mode Exit fullscreen mode

如果你决定做一些不同的事情,例如:

import React from 'react'
import hoistNonReactStatics from 'hoist-non-react-statics'
import getDisplayName from 'utils/getDisplayName'

const useFreeMoney = (WrappedComponent) => {
  class WithFreeMoney extends React.Component {
    giveFreeMoney() {
      return 50000
    }

    render() {
      return (
        <WrappedComponent
          additionalMoney={[
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
          ]}
          {...this.props}
        />
      )
    }
  }

  WithFreeMoney.displayName = `useFreeMoney(${getDisplayName(
    WrappedComponent,
  )}$)`
  hoistNonReactStatics(WithFreeMoney, WrappedComponent)

  return WithFreeMoney
}

export default useFreeMoney
Enter fullscreen mode Exit fullscreen mode

这是完全有效的 JavaScript,这样命名也没什么问题。不过,已经有了标准的命名约定,例如React Hooksuse就已实现。分享代码时务必谨慎,尤其是在寻求帮助时,因为人们可能已经习惯了这些约定俗成的约定。

4. 避免布尔陷阱

在决定输出时,如果要使用原始布尔值来确定某个输出值,就必须格外小心。这被称为代码异味,它会迫使开发人员查看组件的源代码/实现,才能对最终结果做出准确的假设。

例如,如果我们声明了一个 Typography 组件,采用以下可用选项'h1',,,,,,,,,'h2''h3''h4''h5''h6''title''subheading'

当它们以这种方式传递时,您如何知道它们将如何被应用?

const App = () => (
  <Typography color="primary" align="center" subheading title>
    Welcome to my bio
  </Typography>
)
Enter fullscreen mode Exit fullscreen mode

那些对 React(或者更确切地说, JavaScript )更有经验的人可能已经猜到它将title继续进行,subheading因为按照排序的方式,最后一个将覆盖前一个。

但问题是,如果不查看源代码,我们就无法真正知道它将应用到什么程度titlesubheading

例如:

.title {
  font-size: 1.2rem;
  font-weight: 500;
  text-transform: uppercase;
}

.subheading {
  font-size: 1.1rem;
  font-weight: 400;
  text-transform: none !important;
}
Enter fullscreen mode Exit fullscreen mode

即使title“胜出”,text-transform: uppercaseCSS 代码仍然不会被应用,因为在其实现中subheading声明了更高的优先级text-transform: none !important;。如果我们不够小心,调试样式问题可能会变得非常困难,尤其是在它不会在控制台中显示任何警告/错误的情况下。这会使组件的签名变得复杂。

以下只是一个更清晰的替代方案的示例,用于重新实现Typography解决问题的组件:

const App = () => <Typography variant="title">Welcome to my bio</Typography>
Enter fullscreen mode Exit fullscreen mode

排版

import React from 'react'
import cx from 'classnames'
import styles from './styles.css'

const Typography = ({
  children,
  color = '#333',
  align = 'left',
  variant,
  ...otherProps
}) => {
  return (
    <div
      className={cx({
        [styles.h1]: variant === 'h1',
        [styles.h2]: variant === 'h2',
        [styles.h3]: variant === 'h3',
        [styles.h4]: variant === 'h4',
        [styles.h5]: variant === 'h5',
        [styles.h6]: variant === 'h6',
        [styles.title]: variant === 'title',
        [styles.subheading]: variant === 'subheading',
      })}
    >
      {children}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

现在,当我们variant="title"传入App组件时,我们将确保只会title应用它,并且它省去了我们查看源代码来确定结果的麻烦。

您还可以执行简单的 if/else 来计算 prop:

let result
if (variant === 'h1') result = styles.h1
else if (variant === 'h2') result = styles.h2
else if (variant === 'h3') result = styles.h3
else if (variant === 'h4') result = styles.h4
else if (variant === 'h5') result = styles.h5
else if (variant === 'h6') result = styles.h6
else if (variant === 'title') result = styles.title
else if (variant === 'subheading') result = styles.subheading
Enter fullscreen mode Exit fullscreen mode

但这样做的最大好处是,您只需执行这个简单、干净的一行代码即可:

const result = styles[variant]
Enter fullscreen mode Exit fullscreen mode

5. 使用粗箭头函数

使用胖箭头函数是在 JavaScript 中声明函数的一种更简短、更简洁的方式(在这种情况下更适合称为函数表达式)。

但是,有些时候您不想在函数表达式上使用粗箭头函数,例如当您需要提升时。

在 React 中,同样的概念也适用。但是,如果你不需要提升,那么使用箭头语法是一个更好的选择(在我看来):

// Function declaration version
function Gallery({ title, images = [], ...otherProps }) {
  return (
    <CarouselContext.Provider>
      <Carousel>
        {images.map((src, index) => (
          <img src={src} key={`img_${index}`} />
        ))}
      </Carousel>
    </CarouselContext.Provider>
  )
}

// Arrow / Function expression version
const Gallery = ({ title, images = [], ...otherProps }) => (
  <CarouselContext.Provider>
    <Carousel>
      {images.map((src, index) => (
        <img src={src} key={`img_${index}`} />
      ))}
    </Carousel>
  </CarouselContext.Provider>
)
Enter fullscreen mode Exit fullscreen mode

但是你很难在这个例子中看出它的好处......当你执行简单的单行代码时,箭头函数的美妙之处就会显现出来:

// Function declaration version
function GalleryPage(props) {
  return <Gallery {...props} />
}

// Arrow / Function expression version
const GalleryPage = (props) => <Gallery {...props} />
Enter fullscreen mode Exit fullscreen mode

一句话就能让每个人都开心!:)

6. 将独立函数放在自定义 Hook 之外

我发现有些人在自定义钩子中声明一些函数,而实际上这些函数并不需要。这会导致自定义钩子变得臃肿,并且随着钩子长度的增加,阅读起来也会更加困难,因为有些开发人员可能会开始质疑这个钩子是否真的依赖于钩子内部的函数。如果不是,最好将其移到钩子外部,这样就能清楚地了解钩子依赖哪些函数,哪些不依赖。

以下是一个例子:

import React from 'react'

const initialState = {
  initiated: false,
  images: [],
}

const reducer = (state, action) => {
  switch (action.type) {
    case 'initiated':
      return { ...state, initiated: true }
    case 'set-images':
      return { ...state, images: action.images }
    default:
      return state
  }
}

const usePhotosList = ({ imagesList = [] }) => {
  const [state, dispatch] = React.useReducer(reducer, initialState)

  const removeFalseyImages = (images = []) =>
    images.reduce((acc, img) => (img ? [...acc, img] : acc), [])

  React.useEffect(() => {
    const images = removeFalseyImages(imagesList)
    dispatch({ type: 'initiated' })
    dispatch({ type: 'set-images', images })
  }, [])

  return {
    ...state,
  }
}

export default usePhotosList
Enter fullscreen mode Exit fullscreen mode

看一下这个例子,removeFalseyImages实际上并不需要放在自定义钩子里面,而是可以从外面提取出来,而且仍然可以在钩子里面毫无问题地使用,因为它不与任何状态交互。

7.保持一致

保持一致也是 JavaScript 中常用的推荐方法。

对于 React,请保持一致:

  1. 进出口
  2. 命名组件、钩子、HOC、类名

当导入和导出组件时,我有时喜欢使用这种语法来将导出放在中间:

import App from './App'

export { default as Breadcrumb } from './Breadcrumb'

export default App
Enter fullscreen mode Exit fullscreen mode

但我同样喜欢这个语法:

export { default } from './App'
export { default as Breadcrumb } from './Breadcrumb'
Enter fullscreen mode Exit fullscreen mode

无论你喜欢做哪一个,只要确保你为每个项目选择一个一致的方案,以保持其简单即可。

保持一致的命名约定也是一条非常重要的规则。

当你定义一个钩子时useApp,用前缀来命名下一个钩子是很重要useuseController

如果你不这样做,你最终会做这样的事情:

// custom hook #1
const useApp = ({ data: dataProp = null }) => {
  const [data, setData] = React.useState(dataProp)

  React.useEffect(() => {
    setData(data)
  }, [])

  return {
    data,
  }
}

// custom hook #2
const basicController = ({ device: deviceProp }) => {
  const [device, setDevice] = React.useState(deviceProp)

  React.useEffect(() => {
    if (!device && deviceProp) {
      setDevice(deviceProp === 'mobile' ? 'mobile' : 'desktop')
    }
  }, [deviceProp])

  return {
    device,
  }
}
Enter fullscreen mode Exit fullscreen mode

导入两个钩子:

import React from 'react'
import useApp from './useApp'
import basicController from './basicController'

const App = () => {
  const app = useApp()
  const controller = basicController()

  return (
    <div>
      {controller.errors.map((errorMsg) => (
        <div>{errorMsg}</div>
      ))}
    </div>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

这不是一眼就能看出这basicController是一个自定义 React Hook,useApp而是迫使开发人员仔细阅读代码才能真正弄清楚真相。如果我们保持一致,就不会出现这种情况,因为我们可以让它显而易见:

const app = useApp()
const controller = useBasicController()
Enter fullscreen mode Exit fullscreen mode

8. 组件化重复元素

组件化只是“将重复元素转换为其自己的可重用组件”的一种奇特说法。

每个人在 React 中编写重复代码都有其原因,无论是有意还是无意。

无论原因是什么,最好不要留下大量未受影响的重复代码。

首先,你可能已经养成了重复操作的习惯,因为你之前根本不在乎重复的代码。你这样做,怎么能算得上一个优秀的团队成员呢?你还会给你的队友带来负担,因为他们以后看到重复的元素可能会感到沮丧,甚至会感到困惑,尤其是在编辑这些元素的时候。

最糟糕的是,他们因为重复的代码而受到批评,尽管这些代码根本就不是他们写的。如果他们真的这么做了,那就代表你为团队承担一部分责任吧。以后避免重复代码就是对他们的回报!

让我们看一下下面的代码并将重复的部分组件化:

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>
    <div>
      <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>
    </div>
  </Body>
)
Enter fullscreen mode Exit fullscreen mode

现在如果有人告诉你将网格大小从 更改xs={12} sm={6}xs={12} sm={4}将会很麻烦,因为你必须更改四次。

补偿的优点在于,您只需进行一次更改,它就会反映在所有网格中:

const SomeComponent = ({ classes, xs = 12, sm = 6, md, lg }) => {
  const BodySection = ({ header, src }) => {
    const gridSizes = { xs, sm, md, lg }
    return (
      <Section height={500}>
        <Grid spacing={16} container>
          <Grid {...gridSizes} item>
            <div className={classes.groupsHeader}>
              <Header center>{header}</Header>
            </div>
          </Grid>
          <Grid {...gridSizes} item>
            <div>
              <img src={src} alt="" className={classes.img} />
            </div>
          </Grid>
        </Grid>
      </Section>
    )
  }

  return (
    <Body noBottom>
      <Header center>Title</Header>
      <Divider />
      <Background grey>
        <BodySection header="Groups" src={photos.groups} />
      </Background>
      <div>
        <BodySection header="Labs" src={photos.labs} />
      </div>
    </Body>
  )
}
Enter fullscreen mode Exit fullscreen mode

在最基本的提取层面上,这对于人类来说变得更加容易阅读和维护,同时仍然保持正常的实施!

9. 保持组件简单

我在生产 Web 应用程序时学到的不是保持组件简单,而是避免使组件变得复杂。

下面是一个不必要复杂的组件的示例:

ConfirmAvailability.js

import React from 'react'
import Grid from '@material-ui/core/Grid'
import Typography from '@material-ui/core/Typography'
import MenuItem from '@material-ui/core/MenuItem'
import Select from '@material-ui/core/Select'
import Time from 'util/time'

/**
 * Timezone picker. Automatically detects the timezone from the client's device but also displays
 * a clock using this timezone to make sure it is correct. If not, the user may override it.
 *
 * NOTE: Be careful about Date().getTimezoneOffset(). It does two things differently from standard
 *      1. Time difference is in minutes
 *      2. Time difference is from local to UTC, not UTC to local. This means it will be negative of
 *          the expected UTC format
 */
export default class TimeZonePicker extends React.Component {
  state = {
    time: new Date(),
    offset: -(new Date().getTimezoneOffset() / 60),
  }

  componentDidMount() {
    this.props.setOffset(this.state.offset)
  }

  handleChange = (event) => {
    const d = new Date()
    d.setTime(
      d.getTime() +
        d.getTimezoneOffset() * 60 * 1000 +
        event.target.value * 3600 * 1000,
    )
    this.setState({
      time: d,
      offset: event.target.value,
    })
    this.props.setOffset(event.target.value)
  }

  render() {
    const timezones = []
    for (let i = -12; i <= 14; i++) {
      timezones.push(
        <MenuItem key={i} value={i}>
          {i > 0 ? '+' : null}
          {i}
        </MenuItem>,
      )
    }

    return (
      <React.Fragment>
        <Grid container justify="space-between">
          <div>
            <Typography>Current time</Typography>
            <Typography variant="h6" gutterBottom>
              {Time.formatTime(this.state.time)}
            </Typography>
          </div>
          <div>
            <Typography>Set timezone</Typography>
            <Select value={this.state.offset} onChange={this.handleChange}>
              {timezones}
            </Select>
          </div>
        </Grid>
      </React.Fragment>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

该组件原本设计为一个简单的组件,但由于逻辑紧密耦合,它负责多项功能。编写此代码时,React Hooks 尚未发布,但仍然有高阶组件和渲染 props。因此,我们将使用其中一种模式将其重写得更简单,以演示如何使组件更简单(而不更改功能):

SelectTimeZone.js

import React from 'react'

/**
 * Timezone picker. Automatically detects the timezone from the client's device but also displays
 * a clock using this timezone to make sure it is correct. If not, the user may override it.
 *
 * NOTE: Be careful about Date().getTimezoneOffset(). It does two things differently from standard
 *      1. Time difference is in minutes
 *      2. Time difference is from local to UTC, not UTC to local. This means it will be negative of
 *          the expected UTC format
 */

class SelectTimeZone extends React.Component {
  state = {
    time: new Date(),
    offset: -(new Date().getTimezoneOffset() / 60),
  }

  componentDidMount() {
    this.props.setOffset(this.state.offset)
  }

  handleChange = (event) => {
    const d = new Date()
    d.setTime(
      d.getTime() +
        d.getTimezoneOffset() * 60 * 1000 +
        event.target.value * 3600 * 1000,
    )
    this.setState({
      time: d,
      offset: event.target.value,
    })
    this.props.setOffset(event.target.value)
  }

  getTimeZones = () => {
    const timezones = []
    for (let i = -12; i <= 14; i++) {
      timezones.push(
        <MenuItem key={i} value={i}>
          {i > 0 ? '+' : null}
          {i}
        </MenuItem>,
      )
    }
    return timezones
  }

  render() {
    return this.props.render({
      ...this.state,
      getTimeZones: this.getTimeZones,
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

TimeZonePicker.js

import React from 'react'
import Grid from '@material-ui/core/Grid'
import Typography from '@material-ui/core/Typography'
import MenuItem from '@material-ui/core/MenuItem'
import Select from '@material-ui/core/Select'
import Time from 'util/time'

const TimeZonePicker = () => (
  <SelectTimeZone
    render={({ time, offset, getTimeZones, handleChange }) => (
      <Grid container justify="space-between">
        <div>
          <Typography>Current time</Typography>
          <Typography variant="h6" gutterBottom>
            {Time.formatTime(time)}
          </Typography>
        </div>
        <div>
          <Typography>Set timezone</Typography>
          <Select value={offset} onChange={handleChange}>
            {getTimeZones()}
          </Select>
        </div>
      </Grid>
    )}
  />
)

export default TimeZonePicker
Enter fullscreen mode Exit fullscreen mode

现在我们有了一个更简洁的方法,并从其表现层中提取出了逻辑。对这些组件进行单元测试现在变得容易多了

10.useReducer如果useState变得复杂,请使用

当您需要跟踪多个状态时,使用useState就会开始变得更加难以管理。

这看起来像这样:

import React from 'react'
import axios from 'axios'

const useFrogs = () => {
  const [fetching, setFetching] = React.useState(false)
  const [fetched, setFetched] = React.useState(false)
  const [fetchError, setFetchError] = React.useState(null)
  const [timedOut, setTimedOut] = React.useState(false)
  const [frogs, setFrogs] = React.useState(null)
  const [params, setParams] = React.useState({ limit: 50 })
  const timedOutRef = React.useRef()

  function updateParams(newParams) {
    if (newParams != undefined) {
      setParams(newParams)
    } else {
      console.warn(
        'You tried to update state.params but the parameters were null or undefined',
      )
    }
  }

  function formatFrogs(newFrogs) {
    const formattedFrogs = newFrogs.reduce((acc, frog) => {
      const { name, age, size, children } = frog
      if (!(name in acc)) {
        acc[name] = {
          age,
          size,
          children: children.map((child) => ({
            name: child.name,
            age: child.age,
            size: child.size,
          })),
        }
      }
      return acc
    }, {})
    return formattedFrogs
  }

  function addFrog(name, frog) {
    const nextFrogs = {
      ...frogs,
      [name]: frog,
    }
    setFrogs(nextFrogs)
  }

  function removeFrog(name) {
    const nextFrogs = { ...frogs }
    if (name in nextFrogs) delete nextFrogs[name]
    setFrogs(nextFrogs)
  }

  React.useEffect(() => {
    if (frogs === null) {
      if (timedOutRef.current) clearTimeout(timedOutRef.current)

      setFetching(true)

      timedOutRef.current = setTimeout(() => {
        setTimedOut(true)
      }, 20000)

      axios
        .get('https://somefrogsaspi.com/api/v1/frogs_list/', { params })
        .then((response) => {
          if (timedOutRef.current) clearTimeout(timedOutRef.current)
          setFetching(false)
          setFetched(true)
          if (timedOut) setTimedOut(false)
          if (fetchError) setFetchError(null)
          setFrogs(formatFrogs(response.data))
        })
        .catch((error) => {
          if (timedOutRef.current) clearTimeout(timedOutRef.current)
          console.error(error)
          setFetching(false)
          if (timedOut) setTimedOut(false)
          setFetchError(error)
        })
    }
  }, [])

  return {
    fetching,
    fetched,
    fetchError,
    timedOut,
    frogs,
    params,
    addFrog,
    removeFrog,
  }
}

export default useFrogs
Enter fullscreen mode Exit fullscreen mode

如果将其转换为useReducer:这将变得更易于管理:

import React from 'react'
import axios from 'axios'

const initialFetchState = {
  fetching: false
  fetched: false
  fetchError: null
  timedOut: false
}

const initialState = {
  ...initialFetchState,
  frogs: null
  params: { limit: 50 }
}

const reducer = (state, action) => {
  switch (action.type) {
    case 'fetching':
      return { ...state, ...initialFetchState, fetching: true }
    case 'fetched':
      return { ...state, ...initialFetchState, fetched: true, frogs: action.frogs }
    case 'fetch-error':
      return { ...state, ...initialFetchState, fetchError: action.error }
    case 'set-timed-out':
      return { ...state, ...initialFetchState, timedOut: true }
    case 'set-frogs':
      return { ...state, ...initialFetchState, fetched: true, frogs: action.frogs }
    case 'add-frog':
      return { ...state, frogs: { ...state.frogs, [action.name]: action.frog }}
    case 'remove-frog': {
      const nextFrogs = { ...state.frogs }
      if (action.name in nextFrogs) delete nextFrogs[action.name]
      return { ...state, frogs: nextFrogs }
    }
    case 'set-params':
      return { ...state, params: { ...state.params, ...action.params } }
      default:
        return state
  }
}

const useFrogs = () => {
  const [state, dispatch] = React.useReducer(reducer, initialState)
  const timedOutRef = React.useRef()

  function updateParams(params) {
    if (newParams != undefined) {
      dispatch({ type: 'set-params', params })
    } else {
      console.warn(
        'You tried to update state.params but the parameters were null or undefined',
      )
    }
  }

  function formatFrogs(newFrogs) {
    const formattedFrogs = newFrogs.reduce((acc, frog) => {
      const { name, age, size, children } = frog
      if (!(name in acc)) {
        acc[name] = {
          age,
          size,
          children: children.map((child) => ({
            name: child.name,
            age: child.age,
            size: child.size,
          })),
        }
      }
      return acc
    }, {})
    return formattedFrogs
  }

  function addFrog(name, frog) {
    dispatch({ type: 'add-frog', name, frog })
  }

  function removeFrog(name) {
    dispatch({ type: 'remove-frog', name })
  }

  React.useEffect(() => {
    if (frogs === null) {
      if (timedOutRef.current) clearTimeout(timedOutRef.current)

      timedOutRef.current = setTimeout(() => {
        setTimedOut(true)
      }, 20000)

      axios
        .get('https://somefrogsaspi.com/api/v1/frogs_list/', { params })
        .then((response) => {
          if (timedOutRef.current) clearTimeout(timedOutRef.current)
          const frogs = formatFrogs(response.data)
          dispatch({ type: 'set-frogs', frogs })
        })
        .catch((error) => {
          if (timedOutRef.current) clearTimeout(timedOutRef.current)
          console.error(error)
          dispatch({ type: 'fetch-error', error })
        })
    }
  }, [])

  return {
    fetching,
    fetched,
    fetchError,
    timedOut,
    frogs,
    params,
    addFrog,
    removeFrog,
  }
}

export default useFrogs
Enter fullscreen mode Exit fullscreen mode

useState尽管从表面上看,这种方法可能并不比该方法更简洁,但是当您使用版本实现自定义钩子时,这种方法更容易管理,因为useReducer您不必担心跟踪钩子多个部分的状态更新,因为您将在reducer.

我们现在还定义了一套“官方”规则,规定如何在函数内部操作 state.frogs reducer,并实现更直接、更清晰的逻辑分离。换句话说,如果我们继续使用useStatethis ,就不会像 中那样,存在一个预定义的实体,因为useReducer所有逻辑都放在 中reducer

在之前的版本中useState,除了编写逻辑之外,我们还必须在钩子内部声明函数,以便确定状态的下一部分。而在这个useReducer版本中,我们无需这样做,而是将它们移到了reducer函数中。我们只需要调用相应的动作类型,其他的就都不用担心了 :)

11. 在枯燥的地方使用函数声明

useEffect清理处理程序就是一个很好的例子:

React.useEffect(() => {
  setMounted(true)

  return () => {
    setMounted(false)
  }
}, [])
Enter fullscreen mode Exit fullscreen mode

对于了解其功能的 React 开发者来说,这没什么问题。但如果你假设其他人会阅读你的代码,那么使用函数声明来明确地表达代码是个好主意,因为我们可以方便地命名它们。例如:

React.useEffect(() => {
  setMounted(true)

  return function cleanup() {
    setMounted(false)
  }
}, [])
Enter fullscreen mode Exit fullscreen mode

这更清楚地描述了返回函数时发生的情况。

12. 使用 Prettier

Prettier帮助您和您的团队保持一致的代码格式。它节省时间和精力,并减少在代码审查中讨论代码风格的需要。它还强制执行干净的代码实践,您可以根据自己对哪些规范合理、哪些规范不合理的意见进行配置。

13. 使用小片段而不是大片段

小碎片

const App = () => (
  <>
    <FrogsTable />
    <FrogsGallery />
  </>
)
Enter fullscreen mode Exit fullscreen mode

大片段

const App = () => (
  <React.Fragment>
    <FrogsTable />
    <FrogsGallery />
  </React.Fragment>
)
Enter fullscreen mode Exit fullscreen mode

14. 把东西整理好

我在编写代码时喜欢做的一件事就是把事情整理好,比如导入文件时(react导入除外):

import React from 'react'
import { useSelector } from 'react-redux'
import styled from 'styled-components'
import FrogsGallery from './FrogsGallery'
import FrogsTable from './FrogsTable'
import Stations from './Stations'
import * as errorHelpers from '../utils/errorHelpers'
import * as utils from '../utils/'
Enter fullscreen mode Exit fullscreen mode

你们有些人可能会想,这甚至不是按字母顺序排列的。这只是排序方案的一部分。

为了获得干净的导入方式,我喜欢按照以下准则按优先顺序排列:

  1. 反应导入
  2. 库导入(按字母顺序)
  3. 从项目绝对导入(按字母顺序)
  4. 相对导入(按字母顺序)
  5. import * as
  6. import './<some file>.<some ext>'

我还喜欢用其他方式对变量进行排序:

const character = (function() {
  return {
    cry() {
      //
    },
    eat() {
      //
    },
    hop() {
      //
    },
    jump() {
      //
    },
    punch() {
      //
    },
    run() {
      //
    },
    scratch() {
      //
    },
    scream() {
      //
    },
    sleep() {
      //
    },
    walk() {
      //
    },
    yawn() {
      //
    },
  }
})()
Enter fullscreen mode Exit fullscreen mode

遵循指南有助于建立更清晰的代码库。

结论

这篇文章到此结束!希望你觉得这篇文章有用,敬请期待!

在Medium上找到我

文章来源:https://dev.to/jsmanifest/14-beneficial-tips-to-write-cleaner-code-in-react-apps-1gcf
PREV
21 个 VSCode 快捷键,让编码更快、更有趣
NEXT
12 个 VSCode 快捷键和策略,简化开发