The Power of React Hooks
September 16th, 2019
React Hooks are a new addition to the React library, and have taken React developers by storm. Hooks allow you to write state logic and use other React features without having to write a class component. You can make your own apps using Hooks alone, a seismic shift for anyone on Team React.
In this article, we'll be building an app which I'll call "Slotify," using just React Hooks.
Slotify will provide a user interface that presents a textarea to insert quotes into any blog post. Newlines (\n) and word count will play a role in the quantity. A "Slotified" post will have a minimum of one and a max of three quotes. A quote can be inserted wherever a slot is available. The user will be able to interact with the slot and type or paste in a quote and author attribution of their choice. When they're done, they can click the save button and the blog post will reload, now including their quotes.
These are the hook apis we will be using: (Basically all of them)
useSlotify
(custom)This is what we'll be building: (Converts a blog post into a blog post with styled quotes, and returns back an HTML source code of the post that includes the styles)
In this tutorial we are going to quickly generate a react project with create-react-app.
(If you want to get a copy of the repository from github, click here).
Go ahead and create a project using the command below. For this tutorial i’ll call our project build-with-hooks.
npx create-react-app build-with-hooks
Now go into the directory once it's done:
cd build-with-hooks
Inside the main entry src/index.js
we're going to clean it up a bit so we can focus on the App
component:
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()
Now go to src/App.js
and lets start with rendering nothing:
import React from 'react'
function App() {
return null
}
export default App
The core functionality of our app is to allow a user to insert/write a blog post into some type of field so that quotes can be inserted.
To make sure that we stay positive and optimistic that we can do this, let's just tackle down the core functionality first so that we know we're in good shape.
That means we're going to first make a button so that the user has the ability to start by clicking it. Then, we're also going to create the textarea
element so that the user can insert content into.
src/Button.js
import React from 'react'
function Button({ children, ...props }) {
return (
<button type="button" {...props}>
{children}
</button>
)
}
export default Button
Inside index.css I applied some styling so that every button
will have the same styles:
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;
}
Lets proceed to create the textarea component. We'll call it 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
We're using inline styles because we want the styles to be included when the final content is generated. If we use pure CSS, only class name strings will be generated, so the components would turn out styleless.
We're going to create a react context to wrap this whole thing from the top so that we force all child components to stay in sync with the rest of the components. We'll do this by using React.useContext
Create a Context.js
file:
src/Context.js
import React from 'react'
const Context = React.createContext()
export default Context
Now we are going to create Provider.js
which will import Context.js
and will hold all the logic in managing state:
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
This last code snippet is very important. We would have used React.useState
to manage our state, but when you think about what our app is going to do, you might realize that it isn't just a single state. This is because there are situations from both sides that need to be taken into consideration:
Knowing this, we ought to use a React.useReducer to design our state to encapsulate state update logic into a single location. Our first action is declared by adding the first switch case accessible by dispatching an action with type 'set-slotified-content'
.
The way we are going to insert slots into the blog post is grabbing a string and converting it to an array delimiting it by newlines '\n'
which is why the initial state declares slotifiedContent
as an array, because that's where we will putting our working data in.
We also see a textareaRef
declared as we need to use it to grab a reference to our PasteBin
component we created earlier. We could have made the textarea completely controlled, but the easiest and most performant way to communicate with that is to just grab a reference to the root textarea
element because all we need to do is grab its value instead of setting states. This will be grabbed from using the ref
prop on textarea
later.
Our slotify
function is invoked when the user presses the Start Quotifying button to slotify their blog post. The intention is to pop up a modal and show them the slots that they can enter their quote/authors into. We use the reference to the PasteBin
component to grab the current value of the textarea and migrate the content to the modal.
We then use two utility functions, attachSlots
and split
to slotify the blog post and use that to set state.slotifiedContent
so that our UI can pick it up and do their job.
We put attachSlots
and split
into a utils.js
file as follows:
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)
}
To apply the textareaRef
to the PasteBin
, we have to use React.useContext
to get the React.useRef hook we declared earlier in useSlotify
:
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
The last thing we are missing is creating the <Slot />
component because we used it inside our context. This slot component is the component that takes in a quote and author from the user. This won't be visible to the user right away because we are going to put it inside the modal component which will open only when the user clicks the Start Quotifying button.
This slot component will be a little tough, but i'll explain what's happening afterwards:
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
The most important part in this file is state.drafting
. We didn't declare this in the context yet, but its purpose is to give us a way to know when to show the user the slots as well as when to show them the final output. When state.drafting
is true (which is going to be the default value), we will show them the slots which are the blocks that they can insert their quote and quote's author to. When they click on the Save button, state.drafting
will switch to false
and we will use that to determine that they want to look at their final output.
We declared an input
parameter with a default value of 'textfield'
because in the future we might want to use other input types to let users insert quotes to besides typing (example: file inputs where we can let them upload images as quotes, etc). For this tutorial we're only going to support 'textfield'
.
So when state.drafting
is true
, <SlotDrafting />
is used by Slot
, and when it's false
, <SlotStatic />
is used. It's better to separate this distinction into separate components so we don't bloat components with a bunch of if/else
conditionals.
Also, although we declared some inline styles for the quote/author input fields, we still applied className={styles.slotQuoteInput}
so that we can style the placeholder since we won't be able to do that with inline styles. (This is okay for the final refurbished content because inputs won't even be generated)
Here is the css for that:
src/styles.module.css
.slotQuoteInput::placeholder {
color: #fff;
font-size: 0.9rem;
}
Let's go back and declare the drafting
state to the context:
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
Now finally lets put this into the App.js
component so we can see what this all looks like so far:
(Note: in this example I used a modal component from semantic-ui-react which is not required for the modal. You can use any modal or create a plain modal of your own using the 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>
)
Before we start up our server we need to declare the modal
states (open/close):
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
And here's what we should have so far:
(Note: The SAVE button is closing the modal in the image, but that was a minor error. It should not close the modal)
Now we're going to change PasteBin
a little to declare a new api using React.useImperativeHandle for the textarea so that we can use it in useSlotify
and we don't bloat the hook with a bunch of functions but instead provide back an encapsulated 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
will also be a React.useRef
which will be placed right next to textareaRef
in the useSlotify
hook:
const [state, dispatch] = React.useReducer(reducer, initialState)
const textareaRef = React.useRef()
const textareaUtils = React.useRef()
We will use this new api in the slotify
function:
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 })
}
Now the next thing we are going to do is that when the user is looking at the slots and we detect that they haven't inserted an author yet, we flash that element to bring more of their attention.
For this, we are going to use React.useLayoutEffect inside the SlotDrafting
component because SlotDrafting contains the author input:
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>
)
}
We probably didn't need the use of useLayoutEffect
here, but it's just for demonstration. It's known to be a good option for style updates. since the hook is invoked after the dom is mounted and has had its mutations updated. The reason it's good for styling reasons is because it's invoked before the next browser repaint whereas the useEffect
hook is invoked afterwards--which can cause a sluggy flashy effect in the UI.
styles:
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;
}
}
On the bottom of the modal we put a SAVE
button which will invoke onSave
from useSlotify
. When the user clicks this, the slots will convert to finalized slots (when drafting === false
). We will also render a button nearby that will copy the source code in HTML to their clipboard so that they can paste the content on their blog post.
So far, here is what we have:
Everything will stay the same, except now we work with CSS class names. For the new css class names they are suffixed with Static
to indicate that they are used when drafting === false
. Here is a slight change to the Slot
component to accomodate the CSS changes:
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>
)
}
And here are the newly added CSS styles:
.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%);
}
Here is what our app looks like now:
The last thing we need to do is add a Close button to close the modal, and a Copy button to copy the source code of their finalized blog post.
Adding the Close button is easy. Just add this button next to the Save button. The Copy button will be placed next to the Close button. These buttons will be given some onClick
handlers:
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>
We should be done when we implement the onCopyFinalContent
function, but we're not yet. We're missing one last step. When we copy the finalized content, which part of the UI are we copying? We can't be copying the entire modal because we don't want the SAVE, CLOSE and COPY buttons in our blog posts or it would look awfully awkward. We have to make another React.useRef
and use that to attach to a specific element that only includes the content we want.
This is why we *used inline styles and not entirely CSS classes because we want the styles to be included in the refurbished version.
Declare modalRef
in useSlotify
:
const textareaRef = React.useRef()
const textareaUtils = React.useRef()
const modalRef = React.useRef()
Attach it to the element that will only contain the content:
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>
)
}
Note: We wrapped ModalContent
with a React.useCallback
because we want the reference to stay the same. If we don't, then the component will be re-rendered and all of the quotes/author values will be reset since the onSave
function updates the state. When the state updates, ModalContent
will re-create itself and making a new fresh empty state which is what we don't want.
And finally, onCopyFinalDraft
will be placed inside the useSlotify
hook that will use the 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)
}
And we are done!
Here is our app now:
And that concludes the end of this post! I hope you found it useful and look out for more in the future!