2020 年 React 中操作和使用组件的 9 种方法

2025-06-09

2020 年 React 中操作和使用组件的 9 种方法

在Medium上找到我

现在是成为 Web 开发者的好时机,因为创新持续快速涌现,尤其是在 JavaScript 社区!React是一个构建复杂用户界面的优秀库,如果您是 React 新手,那么本文或许能帮助您了解它对 Web 开发者的强大功能。如果您对 React 并不陌生,那么您可能不会在这篇文章中找到任何新东西,但我希望您能从这篇文章中有所收获,因为我会尝试揭示使用 React 组件的新旧策略。

在本文中,我们将介绍 2020 年操作和使用 React 组件的 9 种方法。

不用多说,我们开始吧!

1. 组件 prop

让现有组件重新使用其逻辑并将其传递到其他地方的多种方法之一是提供一种通过props将逻辑传递到自定义组件的方法

流行的 React 组件库(如Material-UI)在其提供的几乎每个组件中都经常使用这种策略。

有充分的理由说明为什么这是重新使用逻辑的好方法。

如果您需要示例,请查看Gatsby应用中的PrivateRoute组件。它是一个封装身份验证逻辑的简单组件。如果当前用户未通过身份验证,它会将其重定向到登录屏幕。否则,它将继续渲染从 接收到的组件。由于它会渲染从 传递过来的所有内容,因此您可以将该身份验证逻辑复用到任意数量的组件中。这使得它成为一种简单而强大的路由逻辑处理方法。props.componentprops.component

PS:您还可以传递一个表示 HTML DOM 元素的字符串(如"div""span"),它仍将呈现为组件,因为 react内部调用 React.createElement并将其作为元素传递type

2. 一次调用即可渲染元素、类组件或函数组件

在描述用户界面应该是什么样子时,React 开发团队建议使用 JSX。

但别忘了,使用 JSX 最终只是调用 的语法糖React.createElement。因此,值得一提的是,您也可以安全地使用它React.createElement来创建组件!

React.createElement使用JSX有一些好处。

最让我感兴趣的一个好处是,它让我回归到编写常规 JavaScript 的方式,因为我们又回到了只使用函数的方式。其他好处包括避免 React 处理这个调用,并在一个代码块中访问所有实现细节,这样我们就避免了 JavaScript 必须执行的额外步骤。

react-final-form 背后的团队广泛使用这种模式作为工厂来创建他们的字段组件。

3. 使用高阶组件(HOC)劫持 props

高阶组件曾经作为 React 中复用组件逻辑的高级技术而备受推崇。如今,它依然如此。它们本质上是一些函数,接受一个 React 组件作为参数,并返回一个全新的组件

使用这种方法,你可以在一个“不可见”的中间层中覆盖和劫持组件的 props 。这个“中间层”是高阶组件逻辑的执行之处。它们可以选择覆盖被包装组件的 props,或者控制自身的渲染行为。

为了演示这一点,我编写了一个基本的withAuthValidation 高阶组件,它将被传递一个输入(DeactivatorInput),该输入仅对管理员用户可用。它首先将一个组件作为 props,执行一些身份验证逻辑,如果用户未通过身份验证,它将尝试禁用输入:

import React from 'react'

function isAuthed(token) {
  // some auth logic
}

const withAuthValidation = (WrappedComponent) => {
  return (props) => {
    if (isAuthed(props.token)) {
      return <WrappedComponent {...props} />
    }
    return <WrappedComponent {...props} disabled />
  }
}

const DeactivatorInput = ({ style, ...rest }) => (
  <input
    {...rest}
    style={{
      minWidth: 200,
      border: '1px solid rgba(0, 0, 0, 0.5)',
      borderRadius: 4,
      padding: '6px 12px',
      ...style,
    }}
    placeholder="Search a user to deactivate"
  />
)

// Applies the higher order component. This is the component we use to render
//    in place of DeactivatorInput
const DeactivatorInputWithAuthValidation = withAuthValidation(DeactivatorInput)

function App() {
  return (
    <div>
      <DeactivatorInputWithAuthValidation />
    </div>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

React 中的高阶组件

4. 使用 Render Props 复用组件逻辑

我还记得当render props 首次出现时,它迅速在 React 社区流行起来,并成为一种广泛采用的利用组件重用代码逻辑的模式。

使用此模式可以解决高阶组件试图解决的相同问题。但许多开发人员更喜欢使用 render prop 模式,原因很简单:高阶组件引入了一个需要复制静态方法的问题。

另一个让 render props 备受青睐的好理由是,它不需要像高阶组件那样实例化一个新的组件实例。你只需要使用一个组件来实现这个模式(这让 React 感觉更像原生的):

function AuthValidator({ token, render, ...rest }) {
  if (isAuthed(token)) {
    return render({ authenticated: true })
  }
  return render({ authenticated: false })
}

const DeactivatorInput = ({ style, ...rest }) => (
  <input
    {...rest}
    style={{
      minWidth: 200,
      border: '1px solid rgba(0, 0, 0, 0.5)',
      borderRadius: 4,
      padding: '6px 12px',
      ...style,
    }}
    placeholder="Search a user to deactivate"
  />
)

function App() {
  return (
    <div>
      <AuthValidator
        token="abc123"
        render={({ authenticated }) => (
          <DeactivatorInput disabled={!authenticated} />
        )}
      />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

5. 将子组件作为函数来复用组件逻辑

这基本上与使用 render prop 方法相同,只是看起来不同,因为 react 已经将子项放在打开组件标签和关闭标签之间,因此从逻辑上讲它会留在那里:

function AuthValidator({ token, children, ...rest }) {
  if (isAuthed(token)) {
    return children({ authenticated: true })
  }
  return children({ authenticated: false })
}

const DeactivatorInput = ({ style, ...rest }) => (
  <input
    {...rest}
    style={{
      minWidth: 200,
      border: '1px solid rgba(0, 0, 0, 0.5)',
      borderRadius: 4,
      padding: '6px 12px',
      ...style,
    }}
    placeholder="Search a user to deactivate"
  />
)

function App() {
  return (
    <div>
      <AuthValidator token="abc123">
        {({ authenticated }) => <DeactivatorInput disabled={!authenticated} />}
      </AuthValidator>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

6. 通过多个渲染器函数重用组件逻辑

您不仅限于一个 render/children 函数,还可以有多个:

import React from 'react'
import Topbar from './Topbar'
import Sidebar from './Sidebar'
import About from './About'
import Contact from './Contact'
import PrivacyPolicy from './PrivacyPolicy'
import Dashboard from './Dashboard'
import { Router } from '@reach/router'

function App() {
  return (
    <Router>
      <Dashboard
        topbar={({ authenticated }) => (
          <Router>
            <Topbar path="*" authenticated={authenticated} />
          </Router>
        )}
        sidebar={() => (
          <Router>
            <Sidebar path="*" />
          </Router>
        )}
        view={({ authenticated }) => (
          <Router>
            <About path="/about" />
            <Contact path="/contact" />
            <PrivacyPolicy path="/privacy-policy" />
            <Admin path="/admin" authenticated={authenticated} />
          </Router>
        )}
      />
    </Router>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

然而,我不太喜欢也不推荐这种方法,因为在编写 的渲染方法时,这种方法会非常受限Dashboard。但它在像上面这样的情况下很有用,侧边栏或顶栏不会移动到用户界面的其他任何地方。

7. 使用 React Hooks 重用组件逻辑

随后React Hooks诞生,至今它依然在社区掀起风暴。

Hooks 可以让你解决上面列出的任何问题,并通过使用函数让你回到正常的JavaScript

import React from 'react'

function useStuff() {
  const [data, setData] = React.useState({})

  React.useEffect(() => {
    fetch('https://someapi.com/api/users/')
      .then((response) => setData(response.json()))
      .catch((err) => setData(err))
  }, [])

  return { data, setData }
}

function App() {
  const { data } = useStuff()

  if (data instanceof Error) {
    return <p style={{ color: 'red' }}>Error: {data.message}</p>
  }

  return <div>{JSON.stringify(data, null, 2)}</div>
}
Enter fullscreen mode Exit fullscreen mode

渲染道具引入的一个问题是,当我们渲染嵌套在另一个渲染道具组件下的多个渲染道具组件时,我们会遇到如下所示的“回调地狱” :

import React from 'react'
import ControlPanel from './ControlPanel'
import ControlButton from './ControlButton'

function AuthValidator({ token, render, ...rest }) {
  if (isAuthed(token)) {
    return render({ authenticated: true })
  }
  return render({ authenticated: false })
}

function App() {
  return (
    <div>
      <AuthValidator
        render={({ authenticated }) => {
          if (!authenticated) {
            return null
          }
          return (
            <ControlPanel
              authenticated={authenticated}
              render={({ Container, controls }) => (
                <Container
                  render={({ width, height }) => (
                    <div style={{ width, height }}>
                      {controls.map((options) =>
                        options.render ? (
                          <ControlButton
                            render={({ Component }) => (
                              <Component {...options} />
                            )}
                          />
                        ) : (
                          <ControlButton {...options} />
                        ),
                      )}
                    </div>
                  )}
                />
              )}
            />
          )
        }}
      />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

当使用钩子时,它看起来像这样:

import React from 'react'
import useControlPanel from './useControlPanel'
import ControlButton from './ControlButton'

function useAuthValidator({ token }) {
  const [authenticated, setAuthenticated] = React.useState(null)

  React.useEffect(() => {
    if (isAuthed(token)) setAuthenticated(true)
    else setAuthenticated(false)
  })

  return { authenticated }
}

function App() {
  const { authenticated } = useAuthValidator('abc123')
  const { Container, width, height, controls } = useControlPanel({
    authenticated,
  })

  return (
    <Container>
      <div style={{ width, height }}>
        {controls.map((options) =>
          options.render ? (
            <ControlButton
              render={({ Component }) => <Component {...options} />}
            />
          ) : (
            <ControlButton {...options} />
          ),
        )}
      </div>
    </Container>
  )
}
Enter fullscreen mode Exit fullscreen mode

8. 通过与子组件协同工作来重用组件逻辑

我有时仍然发现人们会质疑,当没有像这样明确传递时,组件将如何接收某些 props :

const DeactivatorInput = ({
  component: Component = 'input',
  style,
  opened,
  open: openModal,
  close: closeModal,
  ...rest
}) => (
  <div>
    <Component
      type="email"
      onKeyPress={(e) => {
        const pressedEnter = e.charCode === 13
        if (pressedEnter) {
          openModal()
        }
      }}
      style={{
        minWidth: 200,
        border: '1px solid rgba(0, 0, 0, 0.5)',
        borderRadius: 4,
        padding: '6px 12px',
        ...style,
      }}
      placeholder="Search a user to deactivate"
      {...rest}
    />
    <Modal isOpen={opened}>
      <h1>Modal is opened</h1>
      <hr />
      <button type="button" onClick={closeModal}>
        Close
      </button>
    </Modal>
  </div>
)

function App() {
  return (
    <ControlPanel>
      <DeactivatorInput />
    </ControlPanel>
  )
}
Enter fullscreen mode Exit fullscreen mode

可以理解的是,由于我们没有看到任何 props 被传递给DeactivatorInput,因此有人质疑该代码的有效性,但实际上有一种方法。

能够根据需要注入额外的道具来对元素做出反应而不仅仅是对组件做出反应,这很好。React.cloneElement能够为您做到这一点:

function ControlPanel({ children, ...rest }) {
  const [opened, setOpened] = React.useState(false)
  const open = () => setOpened(true)
  const close = () => setOpened(false)
  return (
    <div>{React.cloneElement(children, { opened, open, close, ...rest })}</div>
  )
}
Enter fullscreen mode Exit fullscreen mode

React 还在处理子级问题时提供了一些其他实用程序,例如React.Children.toArray可以与React.cloneElement多个子级结合使用:

function ControlPanel({ children, ...rest }) {
  const [opened, setOpened] = React.useState(false)
  const open = () => setOpened(true)
  const close = () => setOpened(false)
  const child = React.Children.toArray(children).map((child) =>
    React.cloneElement(child, { opened, open, close, ...rest }),
  )
  return <div>{child}</div>
}
Enter fullscreen mode Exit fullscreen mode

这种策略在实现复合组件时很常见——然而,现在实现类似功能的更好方法是使用React Context,因为之前的方案的缺点是只有直接子组件才能接收传递的 props,React.cloneElement除非在每个嵌套子组件中执行递归,而这其实没有必要。使用 React Context,你可以将子组件放置在任何位置,无论它们的嵌套程度如何,它们仍然能够为你同步。

Rumble charts是一个成功的例子,它大量使用它React.Children.map来决定孩子的行为。

9. 动态创建深度嵌套的组件

在本节中,我们将讨论递归以及它如何帮助简化使用反应组件的过程。

假设我们有一个包含重复元素的组件,比如导航栏,其中包含一个下拉菜单形式的菜单按钮。一个下拉菜单可以包含多个项目,每个项目又可以包含一个嵌套的下拉菜单,如下所示:

使用嵌套菜单递归来响应下拉菜单

我们不想亲自编写这些嵌套菜单。我们唯一需要做的就是编写递归代码:

import React from 'react'
import Button from '@material-ui/core/Button'
import Menu from '@material-ui/core/Menu'
import MenuItem from '@material-ui/core/MenuItem'
import './styles.css'

const items = [
  { to: '/home', label: 'Home' },
  { to: '/blog', label: 'Blog' },
  { to: '/about', label: 'About' },
  { to: '/contact', label: 'Contact' },
  {
    to: '/help-center',
    label: 'Help Center',
    items: [
      { to: '/privacy-policy', label: 'Privacy Policy' },
      { to: '/tos', label: 'Terms of Service' },
      { to: '/partners', label: 'Partners' },
      {
        to: '/faq',
        label: 'FAQ',
        items: [
          { to: '/faq/newsletter', label: 'Newsletter FAQs' },
          { to: '/faq/career', label: 'Employment/Career FAQs' },
        ],
      },
    ],
  },
]

const MyMenu = React.forwardRef(
  ({ items, anchorEl: anchorElProp, createOnClick, onClose }, ref) => {
    const [anchorEl, setAnchorEl] = React.useState(null)
    return (
      <Menu
        ref={ref}
        open={Boolean(anchorElProp)}
        onClose={onClose}
        anchorEl={anchorElProp}
        anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
        transformOrigin={{ vertical: 'top', horizontal: 'right' }}
      >
        {items.map((item) => (
          <div key={item.to}>
            <MenuItem onMouseEnter={item.items && createOnClick(setAnchorEl)}>
              {item.label}
            </MenuItem>
            {item.items && (
              <MyMenu
                key={item.to}
                items={item.items}
                anchorEl={anchorEl}
                createOnClick={createOnClick}
                onClose={() => setAnchorEl(null)}
              />
            )}
          </div>
        ))}
      </Menu>
    )
  },
)

function App() {
  const [anchorEl, setAnchorEl] = React.useState(null)
  const createOnClick = (callback) => {
    return (e) => {
      e.persist()
      return callback(e.currentTarget)
    }
  }

  return (
    <div>
      <Button onMouseEnter={createOnClick(setAnchorEl)} variant="outlined">
        View More
      </Button>
      <MyMenu
        items={items}
        anchorEl={anchorEl}
        createOnClick={createOnClick}
        onClose={() => setAnchorEl(null)}
      />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

创建这样的组件是使您的组件可重用和动态化的好方法。

在Medium上找到我

链接:https://dev.to/jsmanifest/9-ways-in-react-to-manipulate-and-work-with-components-in-2020-17ii
PREV
JavaScript 中的依赖注入容器
NEXT
Node 中安全会话管理的最佳实践