想創建應用程序,只用React Hooks就夠了

React Hooks是React庫的新增功能,推出後席捲了React開發界。Hooks允許你編寫狀態邏輯並使用其他React功能,同時無需編寫類組件。你可以單獨使用Hooks來製作自己的應用程序,這對React相關的從業者來說是一次重大變革。在本文中,我們將只使用React Hooks來構建一個名爲“Slotify”的應用。

Slotify是做什麼的,又是如何做到的?

Slotify提供一個用戶界面,該界面呈現一個textarea,以在博客文章中插入引用。換行(\n)和字數統計負責定量處理。一篇“Slotified”帖子至少有一個引用,最多三個引用。

只要有插槽(slot),就可以插入引用。用戶將能夠與插槽互動,並輸入或粘貼他們選擇的引用和作者署名。完成後,他們可以單擊保存按鈕,然後博客帖子將重新加載,新版本就包含了引用內容。

以下是我們將要使用的Hooks API,基本上都會用到:

下圖是我們要構建的內容(將博客文章轉換爲帶有樣式引用的博客文章,並返回博文包含樣式的HTML源代碼)。

開工

在本教程中,我們將使用create-react-app快速生成一個React項目,其GitHub存儲庫在這裏:

https://github.com/jsmanifest/build-with-hooks

首先使用下面的命令創建一個項目。在本教程中,我們將這個項目稱爲“build-with-hooks”。

npx create-react-app build-with-hooks

完成後進入目錄:

cd build-with-hooks

我們將對主要條目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()

轉到src/App.js,一開始我們什麼都不渲染:

import React from 'react'

function App() {
  return null
}

export default App

首先我們將創建一個開始按鈕。然後,我們將創建textarea元素以供用戶插入內容到:src/Button.js。

import React from 'react'

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

export default Button

在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;
}

繼續創建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

這裏使用內聯樣式,因爲我們希望在生成最終內容時包括這些樣式。如果我們使用純CSS,則只會生成類名字符串,於是這些組件將變成無樣式的。

我們將創建一個React上下文,從頂層將全部內容包裝起來,這樣我們就能強制所有子組件與其餘組件保持同步。我們將使用React.useContext做到這一點。

創建一個Context.js文件(src/Context.js):

import React from 'react'
const Context = React.createContext()
export default Context

現在我們將創建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

最後一段代碼非常重要。我們本可以使用React.useState來管理狀態,但是當你考慮到應用程序的用途,你可能會意識到它不僅僅是單個狀態。這是因爲兩邊的情況都需要考慮:

  1. 用戶什麼時候想Slotify他們的博客文章?
  2. 我們什麼時候應該展示經過翻新的最終內容?
  3. 我們應該在博客帖子中插入多少個插槽?
  4. 我們什麼時候應該顯示或隱藏插槽?

知道了這一點,我們應該使用React.useReducer設計狀態,以便將狀態更新邏輯封裝到單個位置。我們的第一個動作是通過添加第一個開關案例來聲明的,該案例通過分派類型爲’set-slotified-content’的動作來訪問。

爲了在博客文章中插入插槽,我們的方法是抓取一個字符串並將其轉換爲以換行符’\n’分隔的數組。這就是爲什麼初始狀態將slotifiedContent聲明爲數組的原因,因爲那是我們放置工作數據的地方。

我們還聲明瞭一個textareaRef,因爲我們需要用它來獲取對之前創建的PasteBin組件的引用。我們本可以使textarea完全受控,但與之通信的最簡單和最高效的方法是僅獲取對根textarea元素的引用。我們真正需要做的是獲取其值而不是設置狀態。稍後將在textarea上使用ref prop抓取。

當用戶按下“Start Quotifying”按鈕來對博客文章Slotify時,將調用我們的slotify函數。其行爲是彈出一個模態,並向他們顯示可以輸入引用的插槽。我們使用對PasteBin組件的引用來獲取textarea的當前值並將內容遷移到模態。

然後我們使用兩個實用程序函數(attachSlots和split)來對博客帖子Slotify,並用它來設置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)
}

要將textareaRef應用於PasteBin,我們必須使用React.useContext來獲取之前在useSlotify(src/PasteBin.js)中聲明的React.useRef Hook:

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

最後缺的一件事是Slot/組件,因爲我們在上下文中用到了它。該slot組件接受用戶輸入的引用。用戶不會立即看到它,因爲我們將其放在模態組件中,該組件僅在用戶單擊“Start Quotifying”按鈕時開啓。

這個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

該文件最重要的部分是state.drafting。我們尚未在上下文中聲明它,但其目的是讓我們知道何時向用戶顯示插槽,以及何時向他們顯示最終輸出。當state.drafting爲true時(這將是默認值),我們將向他們顯示可以插入引用的插槽。當他們單擊“Save”按鈕時,state.drafting將切換爲false,我們用它來確認用戶要查看最終輸出了。

我們聲明瞭一個input參數,其默認值爲’textfield’,因爲在將來我們可能要使用鍵入以外的其他輸入類型(例如:文件輸入,我們可以讓用戶在引用中上傳圖像等等)。在本教程中,我們僅支持’textfiled’。

因此當state.drafting爲true時,Slot使用;當它爲false時則使用。最好將這種區別分離到其組件中,這樣我們就不會使用大量if/else條件語句讓組件變得臃腫了。

另外,儘管我們爲引用輸入字段聲明瞭一些內聯樣式,但我們仍然應用className={styles.slotQuoteInput}以便爲佔位符設置樣式,因爲我們無法使用內聯樣式來做到這一點。(這不影響最終翻新的內容,因爲它甚至不會生成輸入。)

下面是src/styles.module.css的CSS:

.slotQuoteInput::placeholder {
  color: #fff;
  font-size: 0.9rem;
}

然後在上下文src/Provider.js中聲明drafting狀態:

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

最後將其放入App.js組件中,以便我們看到目前爲止的情況。

注意:在本示例中我使用了來自semantic-ui-react的模態組件,這不是模態必需的。你可以使用任何模態,也可以使用React Portal API,創建自己的純模態。下面是ssrc/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>
)

在啓動服務器之前,我們需要聲明模態狀態(open/closed):

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

最後輸出成這個樣子:

注意:“Save”按鈕會關閉圖片中的模態,但這是一個小錯誤。它不應關閉模態。

現在我們將稍稍更改PasteBin,以使用React.useImperativeHandle爲textarea聲明一個新API,以在useSlotify中使用。我們不會在hook裏塞一堆函數。相反,我們將提供一個封裝的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

textareaUtils還是一個React.useRef,它被放置在useSlotifyHook中的textareaRef旁邊:

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

我們將在slotify函數(src/Provider.js)中使用以下新API:

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 })
}

當用戶查看插槽時,我們發現他們還沒有插入作者出處,因此我們希望刷新該元素以引起他們的注意。

爲此,我們將在SlotDrafting組件內使用React.useLayoutEffect,因爲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>
  )
}

我們可能不需要在這裏使用useLayoutEffect,但這只是爲了演示。衆所周知,這是一個不錯的樣式更新選項,因爲在掛載dom後會調用Hook並更新其變體。它之所以對樣式有益,是因爲它在下一次瀏覽器重繪之前被調用,而useEffectHook在事後被調用,後者可能會在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;
  }
}

在模態的底部,我們放置了一個SAVE按鈕,該按鈕將從useSlotify調用onSave。當用戶單擊時,插槽將轉換爲最終插槽(drafting=== false時)。我們還將在旁邊渲染一個按鈕,該按鈕會將HTML中的源代碼複製到用戶的剪貼板中,以便他們將內容粘貼到博客文章中。

這次我們會使用CSS類名,其他所有內容都保持不變。新的CSS類名稱有Static後綴,以表示在drafting=== false時使用它們。爲了適應CSS更改,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>
  )
}

這是新添加的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%);
}

現在應用變成了這個樣子:

我們需要做的最後一件事是添加一個“Close”按鈕以關閉模態,以及一個“Copy”按鈕以複製最終博客文章的源代碼。

添加Close按鈕很容易。只需在“Save”按鈕旁邊添加它即可。Copy按鈕位於Close按鈕旁邊。這些按鈕將提供一些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>

似乎我們實現onCopyFinalContent函數後任務就該完成了,但事實並非如此。我們缺少最後一步。複製完成的內容時該複製UI的哪一部分?我們不能複製整個模態,因爲我們不想在博客文章中帶上“Save”“Close”和“Copy”按鈕,看起來很尷尬。我們必須創建另一個React.useRef,並用它來附加僅包含所需內容的特定元素。

這就是爲什麼我們使用內聯樣式,而不是全部使用CSS類的原因。因爲我們希望樣式包含在翻新版本中。

聲明useSlotify中的modalRef:

const textareaRef = React.useRef()
const textareaUtils = React.useRef()
const modalRef = React.useRef()

將其附加到僅包含內容的元素上(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>
  )
}

注意:我們用React.useCallback包裝了ModalContent,因爲我們希望引用保持不變。如果不這樣做,則組件將被重新渲染,所有引用/作者值將被重置,因爲onSave函數會更新狀態。狀態更新後,ModalContent將重建自己,從而創建一個我們想要的新的空狀態。

最後,onCopyFinalDraft將放置在useSlotify Hook中,該Hook將使用modalRef ref(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)
}

最後總算完成了!最後的應用長成這樣:

希望這篇文章能對你有所幫助。

原文鏈接:
https://medium.com/better-programming/the-power-of-react-hooks-7584df3af9fe

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章