Skip to content

Commit

Permalink
finish exercise 6
Browse files Browse the repository at this point in the history
  • Loading branch information
kentcdodds committed Mar 22, 2024
1 parent df450e9 commit ef1b523
Show file tree
Hide file tree
Showing 15 changed files with 317 additions and 95 deletions.
1 change: 1 addition & 0 deletions examples/unnecessary-rerenders/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Unnecessary Rerenders
47 changes: 47 additions & 0 deletions examples/unnecessary-rerenders/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useState } from 'react'
import * as ReactDOM from 'react-dom/client'

function CountButton({
count,
onClick,
}: {
count: number
onClick: () => void
}) {
return <button onClick={onClick}>{count}</button>
}

function NameInput({
name,
onNameChange,
}: {
name: string
onNameChange: (name: string) => void
}) {
return (
<label>
Name: <input value={name} onChange={e => onNameChange(e.target.value)} />
</label>
)
}

function App() {
const [name, setName] = useState('')
const [count, setCount] = useState(0)
const increment = () => setCount(c => c + 1)
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<div>
<CountButton count={count} onClick={increment} />
</div>
<div>
<NameInput name={name} onNameChange={setName} />
</div>
{name ? <div>{`${name}'s favorite number is ${count}`}</div> : null}
</div>
)
}

const rootEl = document.createElement('div')
document.body.append(rootEl)
ReactDOM.createRoot(rootEl).render(<App />)
13 changes: 13 additions & 0 deletions examples/unnecessary-rerenders/reset.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import '@total-typescript/ts-reset'
import '@total-typescript/ts-reset/dom'

// eslint-disable-next-line react/no-typos
import 'react'

declare module 'react' {
interface CSSProperties {
[key: `--${string}`]: string | number
}

function use<T>(context: React.Context<T> | Promise<T>): T
}
20 changes: 20 additions & 0 deletions examples/unnecessary-rerenders/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"include": ["**/*.ts", "**/*.tsx"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2023"],
"isolatedModules": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"module": "ES2022",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"target": "ES2022",
"strict": true,
"noImplicitAny": true,
"allowJs": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"allowImportingTsExtensions": true,
"noEmit": true,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import { ButtonWithTooltip } from './tooltip'
export function MatchingPosts() {
const [searchParams] = useSearchParams()
const query = getQueryParam(searchParams)
// 🐨 make a deferredQuery with useDeferredValue
// 🐨 pass the deferredQuery to getMatchingPosts here
// so React can defer the Cards we render with the matching posts.
const matchingPosts = getMatchingPosts(query)

return (
Expand All @@ -21,6 +24,7 @@ export function MatchingPosts() {
)
}

// 🐨 wrap this in memo
function Card({ post }: { post: BlogPost }) {
const [isFavorited, setIsFavorited] = useState(false)

Expand Down
21 changes: 18 additions & 3 deletions exercises/06.rerenders/01.problem.memo/README.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
# Component Memoization

Click "force re-render"
👨‍💼 We've got a problem. Here's how you reproduce it in our component.

- In browser devtools search for ListItem function call
- In React devtools search for the ListItem in the Ranked view
In this exercise, pull up the React DevTools Profiler and start a recording.
Observe when you click the "force rerender" button, the `CityChooser` and
`ListItem` components are rerendered even though no DOM updates were needed.
This is an unnecessary rerender and a bottleneck in our application (especially
if we want to start showing all of the results rather than just the first 500...
which we do want to do eventually). If you enable 6x throttle on the CPU (under
the Performance tab in Chrome DevTools) then you'll notice the issue is more
stark.

Your job is to optimize the `ListItem` component to be memoized via `memo`. Make
note of the before/after render times.

Make sure to check both the React Profiler and the Chrome DevTools Performance
tab.

As with most of these exercises, the code changes are minimal, but the impact
is significant!
7 changes: 3 additions & 4 deletions exercises/06.rerenders/01.solution.memo/README.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Component Memoization

Click "force re-render"

- In browser devtools you can't find the ListItem function call
- In react devtools you don't see it either
👨‍💼 Great job! You've managed to really improve the performance of our list when
unrelated updates occur. But what if there are updates to the list itself?
Hmm...
38 changes: 37 additions & 1 deletion exercises/06.rerenders/02.problem.comparator/README.mdx
Original file line number Diff line number Diff line change
@@ -1,3 +1,39 @@
# Custom Comparator

Hover over one of the list items and notice they all re-render
👨‍💼 We've improved things so the `ListItem` components don't rerender when there
are unrelated changes, but what if there are changes to the list item state?

Hover over one of the list items and notice they all rerender. But we really
only need the hovered item to rerender (as well as the one that's no longer
highlighted).

So let's add a custom comparator to the `memo` call in `ListItem` to only
rerender when the changed props will affect the output.

Here's an example of the comparator:

```tsx
const Avatar = memo(
function Avatar({ user }: { user: User }) {
return <img src={user.avatarUrl} alt={user.name} />
},
(prevProps, nextProps) => {
const avatarChanged = prevProps.user.avatarUrl !== nextProps.user.avatarUrl
const nameChanged = prevProps.user.name !== nextProps.user.name
return avatarChanged || nameChanged
},
)
```

So even if the user object changes, the `Avatar` component will only rerender if
the `avatarUrl` or `name` properties change.

By default, React just checks the reference of the props, so by providing a
custom comparator, we override that default behavior to have a more fine-grained
control over when the component should rerender.

So let's add a custom comparator to the `ListItem` component so it only rerenders
when absolutely necessary.

Pull up the React Profiler and the DevTools Performance tab to see the impact
of this optimization as you hover over different list items.
3 changes: 2 additions & 1 deletion exercises/06.rerenders/02.solution.comparator/README.mdx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Custom Comparator

Hover over one of the list items and notice only one re-renders
👨‍💼 Great! The performance is better! Unfortunately, that custom comparator is
kinda complex. I wonder if we could simplify it a bit...
42 changes: 42 additions & 0 deletions exercises/06.rerenders/03.problem.primitives/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,43 @@
# Primitives

👨‍💼 I would love to have the performance improvement without all the complexity
of the custom comparator function.

The default comparator function is a simple `===` comparison. So if we changed
the props a bit, we could take advantage of this.

Remember our Avatar example before?

```tsx
const Avatar = memo(
function Avatar({ user }: { user: User }) {
return <img src={user.avatarUrl} alt={user.name} />
},
(prevProps, nextProps) => {
const avatarChanged = prevProps.user.avatarUrl !== nextProps.user.avatarUrl
const nameChanged = prevProps.user.name !== nextProps.user.name
return avatarChanged || nameChanged
},
)
```

We could change the props for this component to be primitives instead of objects.

```tsx
const Avatar = memo(function Avatar({
avatarUrl,
name,
}: {
avatarUrl: string
name: string
}) {
return <img src={avatarUrl} alt={name} />
})
```

And now we can use the default comparator function without specifying our own
because a simple check with `===` will be enough.

Let's do this for our `ListItem`.

And make sure to check the before/after of your work!
3 changes: 3 additions & 0 deletions exercises/06.rerenders/03.solution.primitives/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
# Primitive Values

👨‍💼 Great job! That's much better! We still get the perf benefits with `memo`
without all the complexity of a custom comparator function.
3 changes: 3 additions & 0 deletions exercises/06.rerenders/FINISHED.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
# Optimize Rendering

👨‍💼 When you find yourself rendering a ton of the same component, optimizing that
one component can have an outsized impact on performance. Good job!
Loading

0 comments on commit ef1b523

Please sign in to comment.