Skip to content

Commit ef1b523

Browse files
committed
finish exercise 6
1 parent df450e9 commit ef1b523

File tree

15 files changed

+317
-95
lines changed

15 files changed

+317
-95
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Unnecessary Rerenders
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { useState } from 'react'
2+
import * as ReactDOM from 'react-dom/client'
3+
4+
function CountButton({
5+
count,
6+
onClick,
7+
}: {
8+
count: number
9+
onClick: () => void
10+
}) {
11+
return <button onClick={onClick}>{count}</button>
12+
}
13+
14+
function NameInput({
15+
name,
16+
onNameChange,
17+
}: {
18+
name: string
19+
onNameChange: (name: string) => void
20+
}) {
21+
return (
22+
<label>
23+
Name: <input value={name} onChange={e => onNameChange(e.target.value)} />
24+
</label>
25+
)
26+
}
27+
28+
function App() {
29+
const [name, setName] = useState('')
30+
const [count, setCount] = useState(0)
31+
const increment = () => setCount(c => c + 1)
32+
return (
33+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
34+
<div>
35+
<CountButton count={count} onClick={increment} />
36+
</div>
37+
<div>
38+
<NameInput name={name} onNameChange={setName} />
39+
</div>
40+
{name ? <div>{`${name}'s favorite number is ${count}`}</div> : null}
41+
</div>
42+
)
43+
}
44+
45+
const rootEl = document.createElement('div')
46+
document.body.append(rootEl)
47+
ReactDOM.createRoot(rootEl).render(<App />)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import '@total-typescript/ts-reset'
2+
import '@total-typescript/ts-reset/dom'
3+
4+
// eslint-disable-next-line react/no-typos
5+
import 'react'
6+
7+
declare module 'react' {
8+
interface CSSProperties {
9+
[key: `--${string}`]: string | number
10+
}
11+
12+
function use<T>(context: React.Context<T> | Promise<T>): T
13+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"include": ["**/*.ts", "**/*.tsx"],
3+
"compilerOptions": {
4+
"lib": ["DOM", "DOM.Iterable", "ES2023"],
5+
"isolatedModules": true,
6+
"esModuleInterop": true,
7+
"jsx": "react-jsx",
8+
"module": "ES2022",
9+
"moduleResolution": "Bundler",
10+
"resolveJsonModule": true,
11+
"target": "ES2022",
12+
"strict": true,
13+
"noImplicitAny": true,
14+
"allowJs": true,
15+
"forceConsistentCasingInFileNames": true,
16+
"skipLibCheck": true,
17+
"allowImportingTsExtensions": true,
18+
"noEmit": true,
19+
}
20+
}

exercises/03.concurrent-rendering/01.problem.deferred/posts.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import { ButtonWithTooltip } from './tooltip'
1010
export function MatchingPosts() {
1111
const [searchParams] = useSearchParams()
1212
const query = getQueryParam(searchParams)
13+
// 🐨 make a deferredQuery with useDeferredValue
14+
// 🐨 pass the deferredQuery to getMatchingPosts here
15+
// so React can defer the Cards we render with the matching posts.
1316
const matchingPosts = getMatchingPosts(query)
1417

1518
return (
@@ -21,6 +24,7 @@ export function MatchingPosts() {
2124
)
2225
}
2326

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
# Component Memoization
22

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

5-
- In browser devtools search for ListItem function call
6-
- In React devtools search for the ListItem in the Ranked view
5+
In this exercise, pull up the React DevTools Profiler and start a recording.
6+
Observe when you click the "force rerender" button, the `CityChooser` and
7+
`ListItem` components are rerendered even though no DOM updates were needed.
8+
This is an unnecessary rerender and a bottleneck in our application (especially
9+
if we want to start showing all of the results rather than just the first 500...
10+
which we do want to do eventually). If you enable 6x throttle on the CPU (under
11+
the Performance tab in Chrome DevTools) then you'll notice the issue is more
12+
stark.
13+
14+
Your job is to optimize the `ListItem` component to be memoized via `memo`. Make
15+
note of the before/after render times.
16+
17+
Make sure to check both the React Profiler and the Chrome DevTools Performance
18+
tab.
19+
20+
As with most of these exercises, the code changes are minimal, but the impact
21+
is significant!
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# Component Memoization
22

3-
Click "force re-render"
4-
5-
- In browser devtools you can't find the ListItem function call
6-
- In react devtools you don't see it either
3+
👨‍💼 Great job! You've managed to really improve the performance of our list when
4+
unrelated updates occur. But what if there are updates to the list itself?
5+
Hmm...
Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,39 @@
11
# Custom Comparator
22

3-
Hover over one of the list items and notice they all re-render
3+
👨‍💼 We've improved things so the `ListItem` components don't rerender when there
4+
are unrelated changes, but what if there are changes to the list item state?
5+
6+
Hover over one of the list items and notice they all rerender. But we really
7+
only need the hovered item to rerender (as well as the one that's no longer
8+
highlighted).
9+
10+
So let's add a custom comparator to the `memo` call in `ListItem` to only
11+
rerender when the changed props will affect the output.
12+
13+
Here's an example of the comparator:
14+
15+
```tsx
16+
const Avatar = memo(
17+
function Avatar({ user }: { user: User }) {
18+
return <img src={user.avatarUrl} alt={user.name} />
19+
},
20+
(prevProps, nextProps) => {
21+
const avatarChanged = prevProps.user.avatarUrl !== nextProps.user.avatarUrl
22+
const nameChanged = prevProps.user.name !== nextProps.user.name
23+
return avatarChanged || nameChanged
24+
},
25+
)
26+
```
27+
28+
So even if the user object changes, the `Avatar` component will only rerender if
29+
the `avatarUrl` or `name` properties change.
30+
31+
By default, React just checks the reference of the props, so by providing a
32+
custom comparator, we override that default behavior to have a more fine-grained
33+
control over when the component should rerender.
34+
35+
So let's add a custom comparator to the `ListItem` component so it only rerenders
36+
when absolutely necessary.
37+
38+
Pull up the React Profiler and the DevTools Performance tab to see the impact
39+
of this optimization as you hover over different list items.
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
# Custom Comparator
22

3-
Hover over one of the list items and notice only one re-renders
3+
👨‍💼 Great! The performance is better! Unfortunately, that custom comparator is
4+
kinda complex. I wonder if we could simplify it a bit...
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,43 @@
11
# Primitives
2+
3+
👨‍💼 I would love to have the performance improvement without all the complexity
4+
of the custom comparator function.
5+
6+
The default comparator function is a simple `===` comparison. So if we changed
7+
the props a bit, we could take advantage of this.
8+
9+
Remember our Avatar example before?
10+
11+
```tsx
12+
const Avatar = memo(
13+
function Avatar({ user }: { user: User }) {
14+
return <img src={user.avatarUrl} alt={user.name} />
15+
},
16+
(prevProps, nextProps) => {
17+
const avatarChanged = prevProps.user.avatarUrl !== nextProps.user.avatarUrl
18+
const nameChanged = prevProps.user.name !== nextProps.user.name
19+
return avatarChanged || nameChanged
20+
},
21+
)
22+
```
23+
24+
We could change the props for this component to be primitives instead of objects.
25+
26+
```tsx
27+
const Avatar = memo(function Avatar({
28+
avatarUrl,
29+
name,
30+
}: {
31+
avatarUrl: string
32+
name: string
33+
}) {
34+
return <img src={avatarUrl} alt={name} />
35+
})
36+
```
37+
38+
And now we can use the default comparator function without specifying our own
39+
because a simple check with `===` will be enough.
40+
41+
Let's do this for our `ListItem`.
42+
43+
And make sure to check the before/after of your work!
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
# Primitive Values
2+
3+
👨‍💼 Great job! That's much better! We still get the perf benefits with `memo`
4+
without all the complexity of a custom comparator function.

exercises/06.rerenders/FINISHED.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
# Optimize Rendering
2+
3+
👨‍💼 When you find yourself rendering a ton of the same component, optimizing that
4+
one component can have an outsized impact on performance. Good job!

0 commit comments

Comments
 (0)