Skip to content

Commit a77b46b

Browse files
committed
finish up to exercise 5
1 parent d6068e7 commit a77b46b

File tree

20 files changed

+514
-67
lines changed

20 files changed

+514
-67
lines changed
+8-12
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
# Refs
22

3-
In this step we're going to use a completely different example. We're going to
4-
make a `<Tilt />` component that renders a div and uses the `vanilla-tilt`
5-
library to make it super fancy.
3+
👨‍💼 Our users want a button they can click to increment a count a bunch of times.
4+
They also like fancy things. So we're going to package it in a fancy way.
5+
6+
In this exercise we're going to use a completely different example. We're going
7+
to make a `<Tilt />` component that renders a div and uses the
8+
[`vanilla-tilt` library](https://micku7zu.github.io/vanilla-tilt.js/) to make it
9+
super fancy.
610

711
The thing is, `vanilla-tilt` works directly with DOM nodes to setup event
812
handlers and stuff, so we need access to the DOM node. But because we're not the
@@ -16,12 +20,4 @@ Additionally, we'll need to clean up after ourselves if this component is
1620
unmounted. Otherwise we'll have event handlers dangling around on DOM nodes that
1721
are no longer in the document which can cause a memory leak.
1822

19-
To be clear about the memory leak, just imagine what would happen if we mount a
20-
tilt element, then unmount it (without cleaning up). Those DOM nodes hang around
21-
because the event listeners are still listening to events on them (even though
22-
they're no longer in the document). Then let's say we mount a new one and
23-
unmount that again. Now we have two sets of DOM nodes that are still in memory,
24-
but not being used. We keep doing this over and over again and eventually our
25-
computer is just keeping track of all these DOM nodes we don't need it to.
26-
That's what's called a memory leak. So it's really important we clean up after
27-
ourselves to avoid the performance problems associated with memory leaks.
23+
The emoji will guide you. Enjoy!
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
# Refs
2+
3+
👨‍💼 Great job! Now our users have a fancy counter!
Original file line numberDiff line numberDiff line change
@@ -1 +1,32 @@
11
# Dependencies
2+
3+
👨‍💼 Our users wanted to be able to control `vanilla-tilt` a bit. Some of them
4+
like the speed and glare to look different. So Kellie 🧝‍♂️ added a form that will
5+
allow them to control those values. The trouble is when the users change the
6+
values, the vanilla tilt element isn't updated with the new values.
7+
8+
🦉 React needs to know when it needs to run your effect callback function again.
9+
We do this using the dependency array which is the second argument to
10+
`useEffect`. Whenever values in that array changes, React will call the returned
11+
cleanup function and then invoke the effect callback again.
12+
13+
So far we've provided an empty dependency array which effectively tells React
14+
that the effect doesn't depend on any values from the component. This is why the
15+
effect is only run once when the component mounts and when values change it's
16+
not run again.
17+
18+
By default, if you don't provide a second argument, `useEffect` runs after every
19+
render. While this is probably the right default for correctness, it's far from
20+
optimal in most cases. If you're not careful, it's easy to end up with infinite
21+
loops (imagine if you're calling `setState` in the effect which triggers another
22+
render and so on).
23+
24+
👨‍💼 So what we need to do in this step is let React know that our effect callback
25+
depends on the `vanillaTiltOptions` the user is providing. Let's do that by
26+
passing the `vanillaTiltOptions` in the dependency array.
27+
28+
<callout-warning class="aside">
29+
You'll notice an issue when you've finished this step. If you click the button
30+
to increment the count, the tilt effect is reset! We'll fix this in the next
31+
step.
32+
</callout-warning>

exercises/04.dom/02.problem.deps/index.tsx

+19-20
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
// 1️⃣ 🐨 before you do anything else, head down to the useEffect and fix the
2-
// dependency array!
31
import { useEffect, useRef, useState } from 'react'
42
import * as ReactDOM from 'react-dom/client'
53
import VanillaTilt from 'vanilla-tilt'
@@ -10,8 +8,10 @@ interface HTMLVanillaTiltElement extends HTMLDivElement {
108

119
function Tilt({
1210
children,
13-
// 4️⃣ 🐨 get rid of this rest operator and destructure each prop instead
14-
...options
11+
max = 25,
12+
speed = 400,
13+
glare = true,
14+
maxGlare = 0.5,
1515
}: {
1616
children: React.ReactNode
1717
max?: number
@@ -21,28 +21,20 @@ function Tilt({
2121
}) {
2222
const tiltRef = useRef<HTMLVanillaTiltElement>(null)
2323

24+
const vanillaTiltOptions = {
25+
max,
26+
speed,
27+
glare,
28+
'max-glare': maxGlare,
29+
}
30+
2431
useEffect(() => {
2532
const { current: tiltNode } = tiltRef
2633
if (tiltNode === null) return
27-
const vanillaTiltOptions = {
28-
// 5️⃣ 🐨 get rid of options here and simply pass each individual option
29-
...options,
30-
max: 25,
31-
speed: 400,
32-
glare: true,
33-
'max-glare': 0.5,
34-
}
3534
VanillaTilt.init(tiltNode, vanillaTiltOptions)
3635
return () => tiltNode.vanillaTilt.destroy()
37-
// 2️⃣ 🐨 OH NO! NEVER DISABLE THIS LINT RULE!
38-
// Add the options to fix the original bug
39-
// eslint-disable-next-line react-hooks/exhaustive-deps
36+
// 🐨 Add vanillaTiltOptions to fix the original bug
4037
}, [])
41-
// 3️⃣ 🦉 once you add options to the dependency array though, you'll notice
42-
// another bug... Clicking on the button resets the tilt effect because the
43-
// options object is new every render! 🤦‍♂️
44-
// 6️⃣ 🐨 get rid of the options from the dependency array and add each
45-
// individual option.
4638

4739
return (
4840
<div ref={tiltRef} className="tilt-root">
@@ -112,3 +104,10 @@ function App() {
112104
const rootEl = document.createElement('div')
113105
document.body.append(rootEl)
114106
ReactDOM.createRoot(rootEl).render(<App />)
107+
108+
// 🤫 we'll fix this in the next step!
109+
// (ALMOST) NEVER DISABLE THIS LINT RULE IN REAL LIFE!
110+
/*
111+
eslint
112+
react-hooks/exhaustive-deps: "off",
113+
*/
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
# Dependencies
2+
3+
👨‍💼 Great! Now our users can control the tilt effect and that makes them happy.
4+
But they're annoyed about something...

exercises/04.dom/02.solution.deps/index.tsx

+15-7
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,19 @@ function Tilt({
2121
}) {
2222
const tiltRef = useRef<HTMLVanillaTiltElement>(null)
2323

24+
const vanillaTiltOptions = {
25+
max,
26+
speed,
27+
glare,
28+
'max-glare': maxGlare,
29+
}
30+
2431
useEffect(() => {
2532
const { current: tiltNode } = tiltRef
2633
if (tiltNode === null) return
27-
const vanillaTiltOptions = {
28-
max,
29-
speed,
30-
glare,
31-
'max-glare': maxGlare,
32-
}
3334
VanillaTilt.init(tiltNode, vanillaTiltOptions)
3435
return () => tiltNode.vanillaTilt.destroy()
35-
}, [glare, max, maxGlare, speed])
36+
}, [vanillaTiltOptions])
3637

3738
return (
3839
<div ref={tiltRef} className="tilt-root">
@@ -102,3 +103,10 @@ function App() {
102103
const rootEl = document.createElement('div')
103104
document.body.append(rootEl)
104105
ReactDOM.createRoot(rootEl).render(<App />)
106+
107+
// 🤫 we'll fix this in the next step!
108+
// (ALMOST) NEVER DISABLE THIS LINT RULE IN REAL LIFE!
109+
/*
110+
eslint
111+
react-hooks/exhaustive-deps: "off",
112+
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Primitive Dependencies
2+
3+
👨‍💼 Our users are annoyed. Whenever they click the incrementing button in the
4+
middle, the tilt effect is reset. You can reproduce this more easily by clicking
5+
one of the corners of the button.
6+
7+
If you add a `console.log` to the `useEffect`, you'll notice that it runs even
8+
when the button is clicked, even if the actual options are unchanged. The reason
9+
is because the `options` object actually _did_ change! This is because the
10+
`options` object is a new object every time the component renders. This is
11+
because of the way we're using the `...` spread operator to collect the options
12+
into a single (brand new) object. This means that the dependency array will
13+
always be different and the effect will always run!
14+
15+
`useEffect` iterates through each of our dependencies and checks whether they
16+
have changed and it uses `Object.is` to do so (this is effectively the same
17+
as `===`). This means that even if two objects have the same properties, they
18+
will not be considered equal if they are different objects.
19+
20+
```tsx
21+
const options1 = { glare: true, max: 25, 'max-glare': 0.5, speed: 400 }
22+
const options2 = { glare: true, max: 25, 'max-glare': 0.5, speed: 400 }
23+
Object.is(options1, options2) // false!!
24+
```
25+
26+
So the easiest way to fix this is by switching from using an object to using the
27+
primitive values directly. This way, the dependency array will only change when
28+
the actual values change.
29+
30+
So please update the `useEffect` to use the primitive values directly. Thanks!
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
Taken from the vanilla-tilt.js demo site:
3+
https://micku7zu.github.io/vanilla-tilt.js/index.html
4+
*/
5+
.tilt-root {
6+
height: 150px;
7+
background-color: red;
8+
width: 200px;
9+
background-image: -webkit-linear-gradient(315deg, #ff00ba 0%, #fae713 100%);
10+
background-image: linear-gradient(135deg, #ff00ba 0%, #fae713 100%);
11+
transform-style: preserve-3d;
12+
will-change: transform;
13+
transform: perspective(1000px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1);
14+
}
15+
.tilt-child {
16+
position: absolute;
17+
width: 50%;
18+
height: 50%;
19+
top: 50%;
20+
left: 50%;
21+
transform: translateZ(30px) translateX(-50%) translateY(-50%);
22+
box-shadow: 0 0 50px 0 rgba(51, 51, 51, 0.3);
23+
background-color: white;
24+
}
25+
.totally-centered {
26+
width: 100%;
27+
height: 100%;
28+
display: flex;
29+
justify-content: center;
30+
align-items: center;
31+
}
32+
33+
.count-button {
34+
width: 100%;
35+
height: 100%;
36+
background: transparent;
37+
border: none;
38+
font-size: 3em;
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { useEffect, useRef, useState } from 'react'
2+
import * as ReactDOM from 'react-dom/client'
3+
import VanillaTilt from 'vanilla-tilt'
4+
5+
interface HTMLVanillaTiltElement extends HTMLDivElement {
6+
vanillaTilt: VanillaTilt
7+
}
8+
9+
function Tilt({
10+
children,
11+
max = 25,
12+
speed = 400,
13+
glare = true,
14+
maxGlare = 0.5,
15+
}: {
16+
children: React.ReactNode
17+
max?: number
18+
speed?: number
19+
glare?: boolean
20+
maxGlare?: number
21+
}) {
22+
const tiltRef = useRef<HTMLVanillaTiltElement>(null)
23+
24+
// 🐨 move this into the useEffect directly
25+
const vanillaTiltOptions = {
26+
max,
27+
speed,
28+
glare,
29+
'max-glare': maxGlare,
30+
}
31+
32+
useEffect(() => {
33+
const { current: tiltNode } = tiltRef
34+
if (tiltNode === null) return
35+
VanillaTilt.init(tiltNode, vanillaTiltOptions)
36+
return () => tiltNode.vanillaTilt.destroy()
37+
// 🐨 instead of passing the options object here, pass each primitive option
38+
}, [vanillaTiltOptions])
39+
40+
return (
41+
<div ref={tiltRef} className="tilt-root">
42+
<div className="tilt-child">{children}</div>
43+
</div>
44+
)
45+
}
46+
47+
function App() {
48+
const [count, setCount] = useState(0)
49+
const [options, setOptions] = useState({
50+
max: 25,
51+
speed: 400,
52+
glare: true,
53+
maxGlare: 0.5,
54+
})
55+
return (
56+
<div className="app">
57+
<form
58+
onSubmit={e => e.preventDefault()}
59+
onChange={event => {
60+
const formData = new FormData(event.currentTarget)
61+
setOptions({
62+
max: formData.get('max') as any,
63+
speed: formData.get('speed') as any,
64+
glare: formData.get('glare') === 'on',
65+
maxGlare: formData.get('maxGlare') as any,
66+
})
67+
}}
68+
>
69+
<div>
70+
<label htmlFor="max">Max:</label>
71+
<input id="max" name="max" type="number" defaultValue={25} />
72+
</div>
73+
<div>
74+
<label htmlFor="speed">Speed:</label>
75+
<input id="speed" name="speed" type="number" defaultValue={400} />
76+
</div>
77+
<div>
78+
<label>
79+
<input id="glare" name="glare" type="checkbox" defaultChecked />
80+
Glare
81+
</label>
82+
</div>
83+
<div>
84+
<label htmlFor="maxGlare">Max Glare:</label>
85+
<input
86+
id="maxGlare"
87+
name="maxGlare"
88+
type="number"
89+
defaultValue={0.5}
90+
/>
91+
</div>
92+
</form>
93+
<br />
94+
<Tilt {...options}>
95+
<div className="totally-centered">
96+
<button className="count-button" onClick={() => setCount(c => c + 1)}>
97+
{count}
98+
</button>
99+
</div>
100+
</Tilt>
101+
</div>
102+
)
103+
}
104+
105+
const rootEl = document.createElement('div')
106+
document.body.append(rootEl)
107+
ReactDOM.createRoot(rootEl).render(<App />)
108+
109+
// 🤫 we'll fix this in the next step!
110+
// (ALMOST) NEVER DISABLE THIS LINT RULE IN REAL LIFE!
111+
/*
112+
eslint
113+
react-hooks/exhaustive-deps: "off",
114+
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Primitive Dependencies
2+
3+
👨‍💼 This is probably one of the more annoying parts about dependency arrays.
4+
Luckily modern React applications don't need to reach for `useEffect` for many
5+
use cases (thanks to frameworks like Remix), but it's important for you to
6+
understand.

0 commit comments

Comments
 (0)