我理想中的 React 组件剖析

2025-05-24

我理想中的 React 组件剖析

import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
import tw from 'twin.macro'

import { USER_ROUTES, useUser } from 'modules/auth'
import { Loader } from 'modules/ui'
import { usePost } from 'modules/posts'

import { EmptyFallback } from './emptyFallback'

const StyledContainer = styled.div`
  ${tw`w-100 m-auto`}
`

const StyledHeading = styled.h1`
  ${tw`text-lg`}
`

type PostProps = {
  id: string
}

export const Post = ({ id }: PostProps): JSX.Element => {
  const [isExpanded, setIsExpanded] = useState(false)

  const { isLoading, isSuccess, post } = usePost({ id })
  const { user } = useUser()

  if (isLoading) {
    return <Loader />
  }

  if (!isLoading && !post) {
    return <EmptyFallback />
  }

  return (
    <StyledContainer>
     <Link to={USER_ROUTES.ACCOUNT}>Back to account, {user.name}</Link>
     <StyledHeading>{post.title}</StyledHeading>
     {post.body}
    </StyledContainer>
  )
}
Enter fullscreen mode Exit fullscreen mode

这就是我编写组件的方式,也是我偏爱的 React 编写方式。这是一种非常具体且适合我的方式——包括使用styled-components。如果您有任何关于如何改进此结构的建议,我乐于倾听。我喜欢改进自己的工作方式,也非常乐意收到反馈。

如果您愿意就这些问题给我反馈,我会在文章中提出问题!

对于任何刚接触 React、JS、开发或 TS 的人来说,都不必担心这些代码到底做了什么。我只是想展示一个复杂的例子。

注:我想再次强调——这是对我有用的方法。我很想听听你的方法!

导入

导入顺序重要吗?其实不重要。但我喜欢制定一些规则,尤其是对于导入语句可能有 20 行甚至更多的大型组件。这种情况发生得比我愿意承认的要多。我的一般做法是:

  1. 无论如何都要做出反应
  2. 第三方库导入(后接新行)
  3. 内部库导入(和别名导入)
  4. 本地进口
// react
import React, { useEffect } from 'react'

// 3rd party libraries
import moment from 'moment'
import styled from 'styled-components'

// internal shared components/utils/libraries
import { ListItems, useItems } from 'modules/ui'

// local
import { EmptyFallback } from './EmptyFallback'
Enter fullscreen mode Exit fullscreen mode

为什么?当你处理大量的导入时,很容易迷失文件正在使用的内容。围绕导入进行约定可以让我更容易一目了然地看到它正在使用的内容。

样式化组件

无论你使用什么库,你都在某个地方编写 CSS 。我很喜欢 styled-components(我们在工作中使用它们)和Tailwind(我在个人项目中使用它们)。Twin允许你将它们组合在一起——这样你就可以根据需要编写自定义 CSS,而 Tailwind 非常适合快速原型设计和生产级应用。两全其美。

我把它们放在最上面,是因为下面的组件通常会用到它们。如果样式组件太多,我倾向于把它们放在同一个文件里styled.ts

我还倾向于在样式组件前添加 前缀Styled。这是我在工作中学到的。它可以快速区分样式组件和功能更丰富的组件。

const StyledContainer = styled.div`
  ${tw`w-full`}

  background-color: ${COLORS.CONTAINER_BACKGROUND};
`

export const SomeComponent = () => {
  // logic
  const items = useItems()

  return (
   <StyledContainer> {/* styled component that does nothing else */}
    <List items={items} /> {/* component with internal logic */}
   </StyledContainer>
  )
}
Enter fullscreen mode Exit fullscreen mode

为什么?我更喜欢同位置的样式,而且把它们放在顶部,这样它们就可以与具有多种样式的组件分开。

问你一个问题:你如何组织你的样式化组件?如果你有一个包含样式化组件的文件,你该怎么称呼它?

组件类型

我通常将组件类型命名为ComponentNamePropsComponentNameReturn大多数情况下,我会跳过“return”部分JSX.Element(不过我确实会Return在 hooks 中使用这个类型!我改天再写)。请查看React TypeScript 速查表,其中包含了我在 TypeScript 和 React 中使用的大部分约定。

这个约定(命名和放置)清楚地表明:

  1. 此类型属于组件
  2. 此类型不可共享
  3. 在哪里找到类型(就在组件上方)

不内联它也是一种风格选择,但你可以:

// I don't like this
const SomeComponent = ({ 
  id,
  isEnabled,
  data,
  filter,
  onClick
}: {
  id: string,
  isEnabled: boolean
  data: DataStructureType
  filter: FilterType
  onClick: () => void
}): JSX.Element => {}

// I do like this
type SomeComponentProps = {
  id: string,
  isEnabled: boolean
  data: DataStructureType
  filter: FilterType
  onClick: () => void
}

const SomeComponent = ({ 
  id,
  isEnabled,
  data,
  filter,
  onClick
}: SomeComponentProps): JSX.Element => {}
Enter fullscreen mode Exit fullscreen mode

我感觉我必须不断地强调:这对我来说特别有效。这背后没有任何科学或研究依据。它并非“更容易推理”(反正大多数时候“更容易推理”的意思是“我喜欢这个”)。

为什么?将组件类型放在组件声明的正上方,可以让一眼就看到组件的用途和签名。

问你一个问题:你们的组件类型是放在一起吗?你们更喜欢把它们放在一起吗?

组件结构

好的,让我们深入研究一下组件的结构。我认为组件通常包含以下几个部分(具体多少取决于你正在做什么):

  1. 本地状态(useState、useReducer、useRef、useMemo 等)
  2. 非 React hooks 和异步/状态获取内容(react-query、apollo、自定义 hooks 等)
  3. useEffect/useLayoutEffect
  4. 后处理设置
  5. 回调/处理程序
  6. 分支路径渲染(加载屏幕、空白屏幕、错误屏幕)
  7. 默认/成功渲染

或多或少,让我们来看一下:

// local state
const [isExpanded, setIsExpanded] = useState(false)

// non-react hooks
const { isLoading, post } = usePost({ id })

// useEffect
useEffect(() => {
  setIsExpanded(false) // close expanded section when the post id changes
}, [id])

// post processing
const snippet = generateSnippet(post)

// callbacks and handlers
const toggleExpanded = (e: Event): void => {
  setIsExpanded((isExpanded) => !isExpanded)
}

// branching path rendering
if (isLoading) {
  return <Loading />
}

if (post && !isExpanded) {
  return (
    <StyledContainer>{snippet}</StyledContainer>
  )
}

// default/success render
return <StyledContainer>
  <h1>{post.title}</h1>
  <div>{post.content}</div>
</StyledContainer>
Enter fullscreen mode Exit fullscreen mode

关于这一点,我做了一些设置,以便逻辑看起来顺畅,并且尽可能提前声明。我认为这里存在相当大的回旋余地,因为真正重要的是在渲染之前声明变量并使用钩子。这对于钩子正常工作至关重要。如果你尝试短路渲染并因此跳过钩子,React 会提醒你这是一个问题。

我还喜欢在声明块的末尾添加处理程序,这样在将其转换为 use 时,我就可以访问可能需要的任何变量useCallback。这也是我使用--const func = () => {}而不是function func() {}-- 的原因,这样可以快速转换为 useCallback,并避免命名函数和 lambda 表达式不匹配。

这样,我们就可以安全地跳转到分支路径渲染,处理加载画面、错误等,而不必担心钩子。这样,我们就可以提前安全地退出渲染。

最后,我将默认/成功渲染保留在底部。

为什么要涵盖很多内容,但老实说,这都是个人偏好,受到 React 始终在组件中运行所有钩子的要求的影响。

问题这与您的标准有很大差异吗?

重构的潜力

你可能会注意到,我的原始组件没有 useEffect 或后处理示例。这是为什么呢?

通常,如果我必须在组件中执行一些提升操作才能获取特定状态下的数据,或者我有相互关联的变量,我喜欢将其隐藏在钩子中。

例如:

type UsePostProps = {
  id: string
}

type UsePostReturn = {
  isExpanded: boolean
  post: PostType
  isLoading: boolean
  toggleExpanded: () => void
}

export const usePost = ({ id }: UsePostProps): UsePostReturn => {
  const [isExpanded, setIsExpanded] = useState(false)
  const { isLoading, data } = useQuery('cache', getPost)

  useEffect(() => {
    setIsExpanded(false)
  }, [id])

  const post = !isLoading && formatPost(data)

  return {
   isExpanded,
   toggleExpanded,
   isLoading,
   post,
  }
}
Enter fullscreen mode Exit fullscreen mode

为什么我不确定。我不喜欢大型组件,也不喜欢功能太多的组件,所以我把逻辑拆分成几个我觉得合适的钩子。

问题:您觉得默认将 API 调用包装在自定义钩子中怎么样?例如,每个唯一调用都有自己的钩子来处理数据转换、相关状态和数据获取。您是否更喜欢将这些逻辑放在组件内部?

想知道文件夹结构吗?

我制作了一个关于这个主题的React 应用程序结构视频。不过现在回想起来,它有一些语法错误,我在录制时并没有注意到。

文章来源:https://dev.to/antjanus/the-anatomy-of-my-ideal-react-component-1lo0
PREV
那些对我作为开发者产生重大影响的书籍
NEXT
我的个人 Git 技巧备忘单