React Hooks 的强大之处 - 在 React 中仅使用此功能创建应用程序

2025-05-25

React Hooks 的强大之处 - 在 React 中仅使用此功能创建应用程序

在Medium上找到我

React Hooks是 React 库的新增功能,自此便迅速被 React 开发者广泛采用。这些 Hooks 允许你编写状态逻辑并使用其他 React 功能,而无需编写类组件。你只需使用 React Hooks 即可构建自己的应用程序,这证明了 Hooks 的概念对于 React 团队来说是一个重大的转折点。

在本文中,我们将构建一个名为Slotify 的应用程序,仅使用反应钩子。

Slotify将为用户提供一个用户界面,呈现一个文本区域,用户可以在其中插入任何博客文章的引用,其中换行符 ( \n) 和字数将影响引用的数量。一篇 Slotify 文章至少包含一条引用,最多包含三条引用。

只要有空位,就可以插入引言及其作者。用户可以与该空位互动,并输入/粘贴自己选择的引言/作者。完成后,点击“保存”按钮,系统就会重新创建包含引言的更新版博客文章。这是用户打算用作下一篇博客文章的最终版本。

这些是我们将要使用的钩子api:(基本上全部)

这就是我们要构建的内容:(将博客文章转换为带有样式引用的博客文章,并返回包含样式的文章的 HTML 源代码)

slotify 仅使用 React Hooks 构建你的应用程序

不用多说,让我们开始吧!

在本教程中,我们将使用 create-react-app 快速生成一个 react 项目。

(如果您想从 github 获取存储库的副本,请单击此处)。

继续使用以下命令创建一个项目。在本教程中,我将我们的项目命名为build-with-hooks

npx create-react-app build-with-hooks
Enter fullscreen mode Exit fullscreen mode

完成后进入目录:

cd build-with-hooks
Enter fullscreen mode Exit fullscreen mode

在主条目中,src/index.js我们将稍微清理一下,以便我们可以专注于App组件:

src/index.js

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import './index.css'
import * as serviceWorker from './serviceWorker'
ReactDOM.render(<App />, document.getElementById('root'))
serviceWorker.unregister()
Enter fullscreen mode Exit fullscreen mode

现在让src/App.js我们开始渲染无:

import React from 'react'

function App() {
  return null
}

export default App
Enter fullscreen mode Exit fullscreen mode

我们的应用程序的核心功能是允许用户在某些类型的字段中插入/写入博客文章,以便可以插入引文。

为了确保我们保持积极乐观的态度,相信我们能够做到这一点,我们首先要解决核心功能,这样我们就知道我们处于良好的状态。

这意味着我们首先要创建一个按钮,以便用户可以通过点击它来开始。然后,我们还将创建一个textarea元素,以便用户可以在其中插入内容。

src/Button.js

import React from 'react'

function Button({ children, ...props }) {
  return (
    <button type="button" {...props}>
      {children}
    </button>
  )
}

export default Button
Enter fullscreen mode Exit fullscreen mode

在 index.css 中我应用了一些样式,以便每个button都有相同的样式:

src/index.css

button {
  border: 2px solid #eee;
  border-radius: 4px;
  padding: 8px 15px;
  background: none;
  color: #555;
  cursor: pointer;
  outline: none;
}

button:hover {
  border: 2px solid rgb(224, 224, 224);
}
button:active {
  border: 2px solid #aaa;
}
Enter fullscreen mode Exit fullscreen mode

让我们继续创建textarea组件。我们将其命名为PasteBin

src/PasteBin.js

import React from 'react'

function PasteBin(props) {
  return (
    <textarea
      style={{
        width: '100%',
        margin: '12px 0',
        outline: 'none',
        padding: 12,
        border: '2px solid #eee',
        color: '#666',
        borderRadius: 4,
      }}
      rows={25}
      {...props}
    />
  )
}

export default PasteBin
Enter fullscreen mode Exit fullscreen mode

现在我们使用内联样式,因为我们希望在最终生成内容时包含这些样式。如果我们使用纯 CSS,则只会生成类名字符串,因此组件将没有样式。

我们将创建一个 React上下文来从顶部包装整个内容,以便我们强制所有子组件能够通过使用React.useContext与其他组件保持同步

创建Context.js文件:

src/Context.js

import React from 'react'

const Context = React.createContext()

export default Context
Enter fullscreen mode Exit fullscreen mode

现在我们将创建Provider.js导入Context.js并保存管理状态中的所有逻辑:

src/Provider.js

import React from 'react'
import Slot from './Slot'
import { attachSlots, split } from './utils'
import Context from './Context'

const initialState = {
  slotifiedContent: [],
}

function reducer(state, action) {
  switch (action.type) {
    case 'set-slotified-content':
      return { ...state, slotifiedContent: action.content }
    default:
      return state
  }
}

function useSlotify() {
  const [state, dispatch] = React.useReducer(reducer, initialState)
  const textareaRef = React.useRef()

  function slotify() {
    let slotifiedContent, content
    if (textareaRef && textareaRef.current) {
      content = textareaRef.current.value
    }
    const slot = <Slot />
    if (content) {
      slotifiedContent = attachSlots(split(content), slot)
    }
    dispatch({ type: 'set-slotified-content', content: slotifiedContent })
  }

  return {
    ...state,
    slotify,
    textareaRef,
  }
}

function Provider({ children }) {
  return <Context.Provider value={useSlotify()}>{children}</Context.Provider>
}

export default Provider
Enter fullscreen mode Exit fullscreen mode

我想花点时间解释一下这最后一段代码的作用,因为它非常重要。我们本来会使用React.useState来管理状态,但是当你思考我们的应用将要做什么时,你可能会意识到它不仅仅是一个状态,因为需要考虑两方面的情况:

  1. 用户何时希望将他们的博客文章插入到插槽中?
  2. 我们应该什么时候向他们展示最终的、翻新的内容?
  3. 我们应该在博客文章中插入多少个插槽?
  4. 我们应该何时显示或隐藏插槽?

了解了这一点,我们应该使用React.useReducer来设计我们的状态,将状态更新逻辑封装到单个位置,因此我们的第一个动作是通过添加可通过分派类型动作访问的第一个 switch case 来声明的'set-slotified-content'

我们将插槽插入博客文章的方式是抓取一个字符串并将其转换为一个用换行符分隔的数组,'\n'这就是初始状态声明slotifiedContent为数组的原因,因为那是我们将工作数据放入的地方。

我们还看到一个声明,因为我们需要用它来获取之前创建的组件textareaRef的引用。我们本来可以让 textarea 完全受控,但与之通信的最简单、最高效的方法是直接获取根元素的引用,因为我们需要做的只是获取它的值,而不是设置状态。稍后使用prop on来获取它。PasteBintextareareftextarea

slotify当用户点击“开始引用”按钮来为博客文章添加引用时,我们的函数就会被调用。目的是弹出一个模态框,向用户展示可以输入引用/作者的槽位。我们使用PasteBin组件的引用来获取文本区域的当前值,并将内容迁移到模态框中。

然后,我们使用两个实用函数,attachSlotssplit博客文章分入插槽并使用它来设置,state.slotifiedContent以便我们的 UI 可以拾取它并完成其工作。

我们将attachSlots和放入如下文件split中:utils.js

src/utils.js

export function attachSlots(content, slot) {
  if (!Array.isArray(content)) {
    throw new Error('content is not an array')
  }
  let result = []
  // Post is too short. Only provide a quote at the top
  if (content.length <= 50) {
    result = [slot, ...content]
  }
  // Post is a little larger but 3 quotes is excessive. Insert a max of 2 quotes
  else if (content.length > 50 && content.length < 100) {
    result = [slot, ...content, slot]
  }
  // Post should be large enough to look beautiful with 3 quotes inserted (top/mid/bottom)
  else if (content.length > 100) {
    const midpoint = Math.floor(content.length / 2)
    result = [
      slot,
      ...content.slice(0, midpoint),
      slot,
      ...content.slice(midpoint),
      slot,
    ]
  }
  return result
}

// Returns the content back as an array using a delimiter
export function split(content, delimiter = '\n') {
  return content.split(delimiter)
}
Enter fullscreen mode Exit fullscreen mode

要将 应用textareaRefPasteBin,我们必须使用React.useContext来获取我们之前在 中声明的React.useRefuseSlotify钩子:

src/PasteBin.js

import React from 'react'
import Context from './Context'

function PasteBin(props) {
  const { textareaRef } = React.useContext(Context)
  return (
    <textarea
      ref={textareaRef}
      style={{
        width: '100%',
        margin: '12px 0',
        outline: 'none',
        padding: 12,
        border: '2px solid #eee',
        color: '#666',
        borderRadius: 4,
      }}
      rows={25}
      {...props}
    />
  )
}

export default PasteBin
Enter fullscreen mode Exit fullscreen mode

我们最后缺少的就是创建<Slot />组件,因为我们在上下文中使用了它。这个插槽组件用于接收用户的引用和作者信息。用户不会立即看到它,因为我们要把它放在模态框组件中,该组件只有当用户点击“开始引用”按钮时才会打开。

这个插槽组件会有点难,但我会解释接下来发生的事情:

import React from 'react'
import PropTypes from 'prop-types'
import cx from 'classnames'
import Context from './Context'
import styles from './styles.module.css'

function SlotDrafting({ quote, author, onChange }) {
  const inputStyle = {
    border: 0,
    borderRadius: 4,
    background: 'none',
    fontSize: '1.2rem',
    color: '#fff',
    padding: '6px 15px',
    width: '100%',
    height: '100%',
    outline: 'none',
    marginRight: 4,
  }

  return (
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-around',
        alignItems: 'center',
      }}
    >
      <input
        name="quote"
        type="text"
        placeholder="Insert a quote"
        style={{ flexGrow: 1, flexBasis: '70%' }}
        onChange={onChange}
        value={quote}
        className={styles.slotQuoteInput}
        style={{ ...inputStyle, flexGrow: 1, flexBasis: '60%' }}
      />
      <input
        name="author"
        type="text"
        placeholder="Author"
        style={{ flexBasis: '30%' }}
        onChange={onChange}
        value={author}
        className={styles.slotQuoteInput}
        style={{ ...inputStyle, flexBasis: '40%' }}
      />
    </div>
  )
}

function SlotStatic({ quote, author }) {
  return (
    <div style={{ padding: '12px 0' }}>
      <h2 style={{ fontWeight: 700, color: '#2bc7c7' }}>{quote}</h2>
      <p
        style={{
          marginLeft: 50,
          fontStyle: 'italic',
          color: 'rgb(51, 52, 54)',
          opacity: 0.7,
          textAlign: 'right',
        }}
      >
        - {author}
      </p>
    </div>
  )
}

function Slot({ input = 'textfield' }) {
  const [quote, setQuote] = React.useState('')
  const [author, setAuthor] = React.useState('')
  const { drafting } = React.useContext(Context)

  function onChange(e) {
    if (e.target.name === 'quote') {
      setQuote(e.target.value)
    } else {
      setAuthor(e.target.value)
    }
  }

  let draftComponent, staticComponent

  if (drafting) {
    switch (input) {
      case 'textfield':
        draftComponent = (
          <SlotDrafting onChange={onChange} quote={quote} author={author} />
        )
        break
      default:
        break
    }
  } else {
    switch (input) {
      case 'textfield':
        staticComponent = <SlotStatic quote={quote} author={author} />
        break
      default:
        break
    }
  }

  return (
    <div
      style={{
        color: '#fff',
        borderRadius: 4,
        margin: '12px 0',
        outline: 'none',
        transition: 'all 0.2s ease-out',
        width: '100%',
        background: drafting
          ? 'rgba(175, 56, 90, 0.2)'
          : 'rgba(16, 46, 54, 0.02)',
        boxShadow: drafting
          ? undefined
          : '0 3px 15px 15px rgba(51, 51, 51, 0.03)',
        height: drafting ? 70 : '100%',
        minHeight: drafting ? 'auto' : 70,
        maxHeight: drafting ? 'auto' : 100,
        padding: drafting ? 8 : 0,
      }}
    >
      <div
        className={styles.slotInnerRoot}
        style={{
          transition: 'all 0.2s ease-out',
          cursor: 'pointer',
          width: '100%',
          height: '100%',
          padding: '0 6px',
          borderRadius: 4,
          display: 'flex',
          alignItems: 'center',
          textTransform: 'uppercase',
          justifyContent: drafting ? 'center' : 'space-around',
          background: drafting
            ? 'rgba(100, 100, 100, 0.35)'
            : 'rgba(100, 100, 100, 0.05)',
        }}
      >
        {drafting ? draftComponent : staticComponent}
      </div>
    </div>
  )
}

Slot.defaultProps = {
  slot: true,
}

Slot.propTypes = {
  input: PropTypes.oneOf(['textfield']),
}

export default Slot
Enter fullscreen mode Exit fullscreen mode

此文件中最重要的部分是state.drafting。我们尚未在上下文中声明它,但它的目的是让我们知道何时向用户显示插槽以及何时显示最终输出。当state.drafting为 true (这将是默认值)时,我们将向用户显示插槽,这些插槽是他们可以插入引言和引言作者的块。当用户点击“保存”按钮时,state.drafting将切换到false,我们将使用它来判断用户是否想要查看最终输出。

我们声明了一个input默认值为 的参数,'textfield'因为将来我们可能希望使用其他输入类型,让用户除了输入之外还能插入引号(例如:文件输入框,我们可以让他们上传图片作为引号等等)。在本教程中,我们仅支持'textfield'

所以,当是 时,state.drafting被 所用;当是 时会被 所用。最好将这种区别分离到单独的组件中,这样我们就不会用一堆条件语句让组件变得臃肿。true<SlotDrafting />Slotfalse<SlotStatic />if/else

另外,虽然我们为引文/作者输入字段声明了一些内联样式,但我们仍然应用了它className={styles.slotQuoteInput},以便能够为占位符添加样式,因为内联样式无法做到这一点。(对于最终翻新的内容来说,这没问题,因为输入框甚至不会生成)

下面是它的 CSS:

src/styles.module.css

.slotQuoteInput::placeholder {
  color: #fff;
  font-size: 0.9rem;
}
Enter fullscreen mode Exit fullscreen mode

让我们回过头来drafting向上下文声明状态:

src/Provider.js

import React from 'react'
import Slot from './Slot'
import { attachSlots, split } from './utils'
import Context from './Context'

const initialState = {
  slotifiedContent: [],
  drafting: true,
}

function reducer(state, action) {
  switch (action.type) {
    case 'set-slotified-content':
      return { ...state, slotifiedContent: action.content }
    case 'set-drafting':
      return { ...state, drafting: action.drafting }
    default:
      return state
  }
}

function useSlotify() {
  const [state, dispatch] = React.useReducer(reducer, initialState)
  const textareaRef = React.useRef()

  function onSave() {
    if (state.drafting) {
      setDrafting(false)
    }
  }

  function setDrafting(drafting) {
    if (drafting === undefined) return
    dispatch({ type: 'set-drafting', drafting })
  }

  function slotify() {
    let slotifiedContent, content
    if (textareaRef && textareaRef.current) {
      content = textareaRef.current.value
    }
    const slot = <Slot />
    if (content && typeof content === 'string') {
      slotifiedContent = attachSlots(split(content), slot)
    }
    dispatch({ type: 'set-slotified-content', content: slotifiedContent })
  }

  return {
    ...state,
    slotify,
    onSave,
    setDrafting,
    textareaRef,
  }
}

function Provider({ children }) {
  return <Context.Provider value={useSlotify()}>{children}</Context.Provider>
}

export default Provider
Enter fullscreen mode Exit fullscreen mode

现在,让我们将其放入App.js组件中,以便我们可以看到到目前为止这一切是什么样的:

(注意:在这个例子中,我使用了semantic-ui-react的模态组件,但该组件对于模态来说不是必需的。您可以使用任何模态,也可以使用React Portal API创建自己的普通模态):

src/App.js

import React from 'react'
import { Modal } from 'semantic-ui-react'
import Button from './Button'
import Context from './Context'
import Provider from './Provider'
import PasteBin from './PasteBin'
import styles from './styles.module.css'

// Purposely call each fn without args since we don't need them
const callFns = (...fns) => () => fns.forEach((fn) => fn && fn())

const App = () => {
  const {
    modalOpened,
    slotifiedContent = [],
    slotify,
    onSave,
    openModal,
    closeModal,
  } = React.useContext(Context)

  return (
    <div
      style={{
        padding: 12,
        boxSizing: 'border-box',
      }}
    >
      <Modal
        open={modalOpened}
        trigger={
          <Button type="button" onClick={callFns(slotify, openModal)}>
            Start Quotifying
          </Button>
        }
      >
        <Modal.Content
          style={{
            background: '#fff',
            padding: 12,
            color: '#333',
            width: '100%',
          }}
        >
          <div>
            <Modal.Description>
              {slotifiedContent.map((content) => (
                <div style={{ whiteSpace: 'pre-line' }}>{content}</div>
              ))}
            </Modal.Description>
          </div>
          <Modal.Actions>
            <Button type="button" onClick={onSave}>
              SAVE
            </Button>
          </Modal.Actions>
        </Modal.Content>
      </Modal>
      <PasteBin onSubmit={slotify} />
    </div>
  )
}

export default () => (
  <Provider>
    <App />
  </Provider>
)
Enter fullscreen mode Exit fullscreen mode

在启动服务器之前,我们需要声明modal状态(打开/关闭):

src/Provider.js

import React from 'react'
import Slot from './Slot'
import { attachSlots, split } from './utils'
import Context from './Context'

const initialState = {
  slotifiedContent: [],
  drafting: true,
  modalOpened: false,
}

function reducer(state, action) {
  switch (action.type) {
    case 'set-slotified-content':
      return { ...state, slotifiedContent: action.content }
    case 'set-drafting':
      return { ...state, drafting: action.drafting }
    case 'open-modal':
      return { ...state, modalOpened: true }
    case 'close-modal':
      return { ...state, modalOpened: false }
    default:
      return state
  }
}

function useSlotify() {
  const [state, dispatch] = React.useReducer(reducer, initialState)
  const textareaRef = React.useRef()

  function onSave() {
    if (state.drafting) {
      setDrafting(false)
    }
  }

  function openModal() {
    dispatch({ type: 'open-modal' })
  }

  function closeModal() {
    dispatch({ type: 'close-modal' })
  }

  function setDrafting(drafting) {
    if (typeof drafting !== 'boolean') return
    dispatch({ type: 'set-drafting', drafting })
  }

  function slotify() {
    let slotifiedContent, content
    if (textareaRef && textareaRef.current) {
      content = textareaRef.current.value
    }
    const slot = <Slot />
    if (content && typeof content === 'string') {
      slotifiedContent = attachSlots(split(content), slot)
    }
    if (!state.drafting) {
      setDrafting(true)
    }
    dispatch({ type: 'set-slotified-content', content: slotifiedContent })
  }

  return {
    ...state,
    slotify,
    onSave,
    setDrafting,
    textareaRef,
    openModal,
    closeModal,
  }
}

function Provider({ children }) {
  return <Context.Provider value={useSlotify()}>{children}</Context.Provider>
}

export default Provider
Enter fullscreen mode Exit fullscreen mode

以下是目前我们应该得到的:

使用 React Hooks 2019 构建你的应用

(注意:图片中的“保存”按钮关闭了模式,但这是一个小错误。它不应该关闭模式)

现在我们将进行PasteBin一些更改,使用React.useImperativeHandle为文本区域声明一个新的 api,以便我们可以使用它useSlotify,并且我们不会用一堆函数来膨胀钩子,而是提供一个封装的 api:

src/PasteBin.js

import React from 'react'
import Context from './Context'

function PasteBin(props) {
  const { textareaRef, textareaUtils } = React.useContext(Context)

  React.useImperativeHandle(textareaUtils, () => ({
    copy: () => {
      textareaRef.current.select()
      document.execCommand('copy')
      textareaRef.current.blur()
    },
    getText: () => {
      return textareaRef.current.value
    },
  }))

  return (
    <textarea
      ref={textareaRef}
      style={{
        width: '100%',
        margin: '12px 0',
        outline: 'none',
        padding: 12,
        border: '2px solid #eee',
        color: '#666',
        borderRadius: 4,
      }}
      rows={25}
      {...props}
    />
  )
}

export default PasteBin
Enter fullscreen mode Exit fullscreen mode

textareaUtils也将是一个将被放置在钩子React.useRef旁边的textareaRefuseSlotify

const [state, dispatch] = React.useReducer(reducer, initialState)
const textareaRef = React.useRef()
const textareaUtils = React.useRef()
Enter fullscreen mode Exit fullscreen mode

我们将在slotify函数中使用这个新的api:

src/Provider.js

function slotify() {
  let slotifiedContent, content
  if (textareaRef && textareaRef.current) {
    textareaUtils.current.copy()
    textareaUtils.current.blur()
    content = textareaUtils.current.getText()
  }
  const slot = <Slot />
  if (content && typeof content === 'string') {
    slotifiedContent = attachSlots(split(content), slot)
  }
  if (!state.drafting) {
    setDrafting(true)
  }
  dispatch({ type: 'set-slotified-content', content: slotifiedContent })
}
Enter fullscreen mode Exit fullscreen mode

现在我们要做的下一件事是,当用户查看插槽时,我们检测到他们尚未插入作者,我们会闪现该元素以引起他们的更多注意。

为此,我们将在组件内部使用React.useLayoutEffectSlotDrafting,因为SlotDrafting包含作者输入:

src/Slot.js

function SlotDrafting({ quote, author, onChange }) {
  const authorRef = React.createRef()

  React.useLayoutEffect(() => {
    const elem = authorRef.current
    if (!author) {
      elem.classList.add(styles.slotQuoteInputAttention)
    } else if (author) {
      elem.classList.remove(styles.slotQuoteInputAttention)
    }
  }, [author, authorRef])

  const inputStyle = {
    border: 0,
    borderRadius: 4,
    background: 'none',
    fontSize: '1.2rem',
    color: '#fff',
    padding: '6px 15px',
    width: '100%',
    height: '100%',
    outline: 'none',
    marginRight: 4,
  }

  return (
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-around',
        alignItems: 'center',
      }}
    >
      <input
        name="quote"
        type="text"
        placeholder="Insert a quote"
        onChange={onChange}
        value={quote}
        className={styles.slotQuoteInput}
        style={{ ...inputStyle, flexGrow: 1, flexBasis: '60%' }}
      />
      <input
        ref={authorRef}
        name="author"
        type="text"
        placeholder="Author"
        onChange={onChange}
        value={author}
        className={styles.slotQuoteInput}
        style={{ ...inputStyle, flexBasis: '40%' }}
      />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

我们可能不需要在useLayoutEffect这里使用,但这只是为了演示。众所周知,它是样式更新的一个好选择。因为这个钩子是在 DOM 挂载并更新其突变后调用的。它之所以适合样式,是因为它是在下次浏览器重绘之前useEffect调用的,而钩子是在之后调用的——这可能会导致UI 出现迟缓的闪烁效果

样式:

src/styles.module.css

.slotQuoteInputAttention {
  transition: all 1s ease-out;
  animation: emptyAuthor 3s infinite;
  border: 1px solid #91ffde;
}

.slotQuoteInputAttention::placeholder {
  color: #91ffde;
}

.slotQuoteInputAttention:hover,
.slotQuoteInputAttention:focus,
.slotQuoteInputAttention:active {
  transform: scale(1.1);
}

@keyframes emptyAuthor {
  0% {
    opacity: 1;
  }
  50% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}
Enter fullscreen mode Exit fullscreen mode

在模态框的底部,我们放置了一个SAVE按钮,用于onSave从调用useSlotify。当用户点击此按钮时,插槽将转换为最终插槽(当 时drafting === false)。我们还会在附近渲染一个按钮,用于将 HTML 源代码复制到剪贴板,以便用户可以将内容粘贴到博客文章中。

到目前为止,我们得到的结果如下:

一切都保持不变,只是现在我们使用 CSS 类名。新的 CSS 类名会添加后缀 ,Static以指示它们在何时使用。为了适应 CSS 的变化,drafting === false我们对组件做了一些细微的改动:Slot

src/Slot.js

function Slot({ input = 'textfield' }) {
  const [quote, setQuote] = React.useState('')
  const [author, setAuthor] = React.useState('')
  const { drafting } = React.useContext(Context)

  function onChange(e) {
    if (e.target.name === 'quote') {
      setQuote(e.target.value)
    } else {
      setAuthor(e.target.value)
    }
  }

  let draftComponent, staticComponent

  if (drafting) {
    switch (input) {
      case 'textfield':
        draftComponent = (
          <SlotDrafting onChange={onChange} quote={quote} author={author} />
        )
        break
      default:
        break
    }
  } else {
    switch (input) {
      case 'textfield':
        staticComponent = <SlotStatic quote={quote} author={author} />
        break
      default:
        break
    }
  }

  return (
    <div
      style={{
        color: '#fff',
        borderRadius: 4,
        margin: '12px 0',
        outline: 'none',
        transition: 'all 0.2s ease-out',
        width: '100%',
        background: drafting
          ? 'rgba(175, 56, 90, 0.2)'
          : 'rgba(16, 46, 54, 0.02)',
        boxShadow: drafting
          ? undefined
          : '0 3px 15px 15px rgba(51, 51, 51, 0.03)',
        height: drafting ? 70 : '100%',
        minHeight: drafting ? 'auto' : 70,
        maxHeight: drafting ? 'auto' : 100,
        padding: drafting ? 8 : 0,
      }}
      className={cx({
        [styles.slotRoot]: drafting,
        [styles.slotRootStatic]: !drafting,
      })}
    >
      <div
        className={styles.slotInnerRoot}
        style={{
          transition: 'all 0.2s ease-out',
          cursor: 'pointer',
          width: '100%',
          height: '100%',
          padding: '0 6px',
          borderRadius: 4,
          display: 'flex',
          alignItems: 'center',
          textTransform: 'uppercase',
          justifyContent: drafting ? 'center' : 'space-around',
          background: drafting
            ? 'rgba(100, 100, 100, 0.35)'
            : 'rgba(100, 100, 100, 0.05)',
        }}
      >
        {drafting ? draftComponent : staticComponent}
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

以下是新添加的 CSS 样式:

.slotRoot:hover {
  background: rgba(245, 49, 104, 0.3) !important;
}

.slotRootStatic:hover {
  background: rgba(100, 100, 100, 0.07) !important;
}

.slotInnerRoot:hover {
  filter: brightness(80%);
}
Enter fullscreen mode Exit fullscreen mode

我们的应用程序现在看起来是这样的:

slotify2

我们需要做的最后一件事是添加一个“关闭”按钮来关闭模式,以及一个“复制”按钮来复制最终博客文章的源代码。

添加“关闭”按钮很简单。只需将此按钮添加到“保存”按钮旁边即可“复制”按钮将放置在“关闭”按钮旁边。这些按钮将被赋予一些onClick处理程序:

src/App.js

<Modal.Actions>
  <Button type="button" onClick={onSave}>
    SAVE
  </Button>
  &nbsp;
  <Button type="button" onClick={closeModal}>
    CLOSE
  </Button>
  &nbsp;
  <Button type="button" onClick={onCopyFinalDraft}>
    COPY
  </Button>
</Modal.Actions>
Enter fullscreen mode Exit fullscreen mode

实现这个功能后,我们应该onCopyFinalContent就完成了,但实际上还没有。我们还差最后一步。复制最终确定的内容时, UI 的一部分是我们要复制的呢?我们不能复制整个模态框,因为我们不想在博客文章中看到“保存”“关闭”“复制”按钮,否则看起来会很别扭。我们必须再创建一个模态框React.useRef,并将其附加到只包含我们所需内容的特定元素上。

这就是为什么我们*使用内联样式而不是完全使用 CSS 类,因为我们希望样式包含在翻新版本中。

声明modalRefuseSlotify

const textareaRef = React.useRef()
const textareaUtils = React.useRef()
const modalRef = React.useRef()
Enter fullscreen mode Exit fullscreen mode

将其附加到仅包含内容的元素

src/App.js

const App = () => {
  const {
    modalOpened,
    slotifiedContent = [],
    slotify,
    onSave,
    openModal,
    closeModal,
    modalRef,
    onCopyFinalContent,
  } = React.useContext(Context)

  const ModalContent = React.useCallback(
    ({ innerRef, ...props }) => <div ref={innerRef} {...props} />,
    [],
  )

  return (
    <div
      style={{
        padding: 12,
        boxSizing: 'border-box',
      }}
    >
      <Modal
        open={modalOpened}
        trigger={
          <Button type="button" onClick={callFns(slotify, openModal)}>
            Start Quotifying
          </Button>
        }
        style={{
          background: '#fff',
          padding: 12,
          color: '#333',
          width: '100%',
        }}
      >
        <Modal.Content>
          <Modal.Description as={ModalContent} innerRef={modalRef}>
            {slotifiedContent.map((content) => (
              <div style={{ whiteSpace: 'pre-line' }}>{content}</div>
            ))}
          </Modal.Description>
          <Modal.Actions>
            <Button type="button" onClick={onSave}>
              SAVE
            </Button>
            &nbsp;
            <Button type="button" onClick={closeModal}>
              CLOSE
            </Button>
            &nbsp;
            <Button type="button" onClick={onCopyFinalContent}>
              COPY
            </Button>
          </Modal.Actions>
        </Modal.Content>
      </Modal>
      <PasteBin onSubmit={slotify} />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

注意:我们ModalContent用 包裹,React.useCallback因为我们希望引用保持不变。如果不这样做,组件将重新渲染,并且所有引用/作者的值都将被重置,因为函数onSave会更新状态。当状态更新时,ModalContent会重新创建自身,从而产生一个新的空状态,而这并不是我们想要的。

最后,onCopyFinalDraft将其放置在useSlotify使用modalRefref 的钩子内:

src/Provider.js

function onCopyFinalContent() {
  const html = modalRef.current.innerHTML
  const inputEl = document.createElement('textarea')
  document.body.appendChild(inputEl)
  inputEl.value = html
  inputEl.select()
  document.execCommand('copy')
  document.body.removeChild(inputEl)
}
Enter fullscreen mode Exit fullscreen mode

我们完成了!

这是我们的应用程序现在的样子:

使用 React Hooks 2019 构建你的 Web 应用

结论

这篇文章到此结束!希望你觉得它有用,并期待未来更多精彩内容!

在Medium上找到我

文章来源:https://dev.to/jsmanifest/build-an-app-with-just-react-hooks-2c45
PREV
JavaScript 中的回调与承诺
NEXT
这是 React v16+ 备忘单(PDF/JPEG/自定义主题)