想创建应用程序,只用React Hooks就够了

2019 年 10 月 12 日

想创建应用程序,只用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 delimiterexport 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 themconst 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


2019 年 10 月 12 日 14:121668

评论

发布
暂无评论
发现更多内容

Istio 中的智能 DNS 代理功能

Jimmy Song

开源 云原生 Service Mesh istio 服务网格

脚手架 | 从零搭建满足权限校验等需求的前端命令行工具

梁龙先森

node.js 前端 前端工程化 前端进阶

week5-(2选1)

Week_09 总结

golangboy

极客大学架构师训练营

极客大学 - 架构师训练营 第十周作业

9527

田哥:面试被问== 与equals 的区别,该怎么回答?

田维常

面试

智能灯串开发资料全开源!为这个冬天装点烂漫“星空”

智能物联实验室

人工智能 物联网 智能硬件 智能家居

第九周作业

Geek_4c1353

极客大学架构师训练营

week5-根据当周学习情况,完成一篇学习总结

经典计算机课程

Joseph295

为什么你写的拦截器注入不了 Java bean?

Java旅途

Java spring Spring Boot 拦截器

鹿鼎记 · 韦小宝,丽春院、天地会、入皇宫等五个场景的恶搞版多线程锁学习!

小傅哥

Java 程序员 小傅哥 多线程

第9周学习总结

饭桶

第九周作业

vivo 调用链 Agent 原理及实践

vivo互联网技术

Java 架构 调用链

Week 9 作业02

Croesus

还在用分库分表?看TiDB如何解决海量数据无感扩容难题

京东智联云开发者

数据库 分布式数据库 #TiDB

讲武德,你们要的高性能日志工具 Log4j2,来了

沉默王二

Java log4j

架构训练营 - 第9周课后作业 - 学习总结

Pudding

LeetCode069-x的平方根-easy

书旅

go 数据结构 算法

第九周总结

熊桂平

【薪火计划】04 - 心态和角色

brave heart

管理

5G革命:如何让「数据」实现最大性能?

VoltDB

数据库 数据分析 5G 工业互联网

《Elasticsearch服务器开发》.pdf

田维常

elasticsearch

Week_09 作业

golangboy

极客大学架构师训练营

第九周课后练习

饭桶

记一次HEX和RGB互换算法的思考及应用

徐小夕

Java 面试 算法 前端

第九周总结

第五周作业

jizhi7

展现非凡领跑力,京东会展云斩获“十大云原生行业落地典范”奖项

京东智联云开发者

云计算 AI 云原生

架构师训练营 - 第 9 周课后作业(1 期)

Pudding

想创建应用程序,只用React Hooks就够了-InfoQ