Skip to content

Commit

Permalink
add flushSync
Browse files Browse the repository at this point in the history
  • Loading branch information
kentcdodds committed Feb 14, 2024
1 parent 1fe4fd9 commit ff10168
Show file tree
Hide file tree
Showing 7 changed files with 240 additions and 0 deletions.
1 change: 1 addition & 0 deletions exercises/08.focus/01.problem/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# flushSync
24 changes: 24 additions & 0 deletions exercises/08.focus/01.problem/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
main {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 3rem;
}
.editable-text {
button {
/* remove button styles. Make it look like text */
background: none;
border: none;
padding: 4px 8px;
font-size: 1.5rem;
font-weight: bold;
}

input {
/* make it the same size as the button */
font-size: 1.5rem;
font-weight: bold;
padding: 4px 8px;
border: none;
}
}
92 changes: 92 additions & 0 deletions exercises/08.focus/01.problem/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { useRef, useState } from 'react'
import * as ReactDOM from 'react-dom/client'

function EditableText({
id,
initialValue = '',
fieldName,
inputLabel,
buttonLabel,
}: {
id?: string
initialValue?: string
fieldName: string
inputLabel: string
buttonLabel: string
}) {
const [edit, setEdit] = useState(false)
const [value, setValue] = useState(initialValue)
const inputRef = useRef<HTMLInputElement>(null)
// 🐨 add a button ref here

return edit ? (
<form
method="post"
onSubmit={event => {
event.preventDefault()
// here's where you'd send the updated value to the server
// 🐨 wrap these calls in a flushSync
setValue(inputRef.current?.value ?? '')
setEdit(false)
// 🐨 after flushSync, focus the button with the button ref
}}
>
<input
required
ref={inputRef}
type="text"
id={id}
aria-label={inputLabel}
name={fieldName}
defaultValue={value}
onKeyDown={event => {
if (event.key === 'Escape') {
// 🐨 wrap this in a flushSync
setEdit(false)
// 🐨 after the flushSync, focus the button
}
}}
onBlur={event => {
// 🐨 wrap these in a flushSync
setValue(event.currentTarget.value)
setEdit(false)
// 🐨 after the flushSync, focus the button
}}
/>
</form>
) : (
<button
aria-label={buttonLabel}
// 🐨 add a ref prop for the button
type="button"
onClick={() => {
// 🐨 wrap this in a flushSync
setEdit(true)
// 🐨 after the flushSync, select all the text of the input
}}
>
{value || 'Edit'}
</button>
)
}

function App() {
return (
<main>
<button>Focus before</button>
<div className="editable-text">
<EditableText
initialValue="Unnamed"
fieldName="name"
inputLabel="Edit project name"
buttonLabel="Edit project name"
/>
</div>
<button>Focus after</button>
</main>
)
}

const rootEl = document.createElement('div')
document.body.append(rootEl)
ReactDOM.createRoot(rootEl).render(<App />)
1 change: 1 addition & 0 deletions exercises/08.focus/01.solution/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# flushSync
24 changes: 24 additions & 0 deletions exercises/08.focus/01.solution/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
main {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 3rem;
}
.editable-text {
button {
/* remove button styles. Make it look like text */
background: none;
border: none;
padding: 4px 8px;
font-size: 1.5rem;
font-weight: bold;
}

input {
/* make it the same size as the button */
font-size: 1.5rem;
font-weight: bold;
padding: 4px 8px;
border: none;
}
}
94 changes: 94 additions & 0 deletions exercises/08.focus/01.solution/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { useRef, useState } from 'react'
import { flushSync } from 'react-dom'
import * as ReactDOM from 'react-dom/client'

function EditableText({
id,
initialValue = '',
fieldName,
inputLabel,
buttonLabel,
}: {
id?: string
initialValue?: string
fieldName: string
inputLabel: string
buttonLabel: string
}) {
const [edit, setEdit] = useState(false)
const [value, setValue] = useState(initialValue)
const inputRef = useRef<HTMLInputElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null)

return edit ? (
<form
method="post"
onSubmit={event => {
event.preventDefault()
// here's where you'd send the updated value to the server
flushSync(() => {
setValue(inputRef.current?.value ?? '')
setEdit(false)
})
buttonRef.current?.focus()
}}
>
<input
required
ref={inputRef}
type="text"
id={id}
aria-label={inputLabel}
name={fieldName}
defaultValue={value}
onKeyDown={event => {
if (event.key === 'Escape') {
flushSync(() => {
setEdit(false)
})
buttonRef.current?.focus()
}
}}
onBlur={event => {
setValue(event.currentTarget.value)
setEdit(false)
}}
/>
</form>
) : (
<button
aria-label={buttonLabel}
type="button"
ref={buttonRef}
onClick={() => {
flushSync(() => {
setEdit(true)
})
inputRef.current?.select()
}}
>
{value || 'Edit'}
</button>
)
}

function App() {
return (
<main>
<button>Focus before</button>
<div className="editable-text">
<EditableText
initialValue="Unnamed"
fieldName="name"
inputLabel="Edit project name"
buttonLabel="Edit project name"
/>
</div>
<button>Focus after</button>
</main>
)
}

const rootEl = document.createElement('div')
document.body.append(rootEl)
ReactDOM.createRoot(rootEl).render(<App />)
4 changes: 4 additions & 0 deletions exercises/08.focus/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Focus Management

This example was uses code from
[trellix](https://github.com/remix-run/example-trellix/blob/3379b3d5e9c0173381031e4f062877e8a3696b2e/app/routes/board.%24id/components.tsx).

0 comments on commit ff10168

Please sign in to comment.