React Hooks是React库的新增功能,推出后席卷了React开发界。Hooks允许你编写状态逻辑并使用其他React功能,同时无需编写类组件。你可以单独使用Hooks来制作自己的应用程序,这对React相关的从业者来说是一次重大变革。在本文中,我们将只使用React Hooks来构建一个名为“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来管理状态,但是当你考虑到应用程序的用途,你可能会意识到它不仅仅是单个状态。这是因为两边的情况都需要考虑:
知道了这一点,我们应该使用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):
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>
<Button type="button" onClick={closeModal}>
CLOSE
</Button>
<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>
<Button type="button" onClick={closeModal}>
CLOSE
</Button>
<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
领取专属 10元无门槛券
私享最新 技术干货