Skip to content

Commit

Permalink
finish instructions
Browse files Browse the repository at this point in the history
  • Loading branch information
kentcdodds committed Jun 29, 2024
1 parent 2e1a649 commit 7376aa5
Show file tree
Hide file tree
Showing 22 changed files with 277 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Initial State

👨‍💼 Great work! Now at least we're not just throwing an error. But let's handle
the state update...
the state update next.
10 changes: 10 additions & 0 deletions exercises/01.use-state/02.problem.update-state/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
# Update State

👨‍💼 Alright, right now when you click the button, nothing happens. Let's get it
to update the state.

Update the `setState` function to assign the given state to the new state.

<callout-warning>
When you do this, it'll not actually update the number in the button either.
We'll get to that soon!
</callout-warning>
3 changes: 3 additions & 0 deletions exercises/01.use-state/02.solution.update-state/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
# Update State

👨‍💼 So we're updating the state value, but it's not actually updating the number
in the button? What gives?!
29 changes: 29 additions & 0 deletions exercises/01.use-state/03.problem.re-render/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,30 @@
# Re-render

👨‍💼 Ok, so we're initializing our state properly and we're updating the state
properly as well. The problem is we're not updating the UI when the state gets
updated. Remember, we're not React. We need to tell React when the state has
changed so it will render our component again.

Because we're not React, the way we will do this is by simply calling `render`
our our root again. Remember what the bottom of our file looks like?

```tsx lines=4
const rootEl = document.createElement('div')
document.body.append(rootEl)
const appRoot = createRoot(rootEl)
appRoot.render(<Counter />)
```

That last line there is where we render the component. So we just need to call
that any time we want the component updated!

So in this exercise, wrap that bit in a function and call it once for the
initial render and once in the `setState` function.

Feel free to toss in a `console.log` in the component to make sure it's
re-rendering.

<callout-warning>
When you're finished with this, the UI will **still** not work like you
expect. I promise we'll get to that very soon!
</callout-warning>
4 changes: 4 additions & 0 deletions exercises/01.use-state/03.solution.re-render/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
# Re-render

👨‍💼 Great work! Now we're not only updating the state, but we're also triggering
a re-render so the UI can be updated. Unfortunately that seems to not be working
either? Let's figure out why.
25 changes: 25 additions & 0 deletions exercises/01.use-state/04.problem.preserve-state/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,26 @@
# Preserve State

👨‍💼 Alright, so there are actually two problems here. First, when the user clicks
on the button, we update the `state` variable inside the `useState` closure, but
that variable is not accessible by our component. Our component has its own
variable called `count` which is not being updated

<callout-info>
Just because two variables point to the same object in memory (or in our case,
the same number) doesn't mean they stay in sync when one is reassigned. That's
just how JavaScript works.
</callout-info>

The second problem we have is when our component is called, it calls `useState`
and that creates a brand new `state` variable that's assigned to the
`initialState` variable again.

So we need a way to preserve the state between renders. We can do that by
pulling the `state` and `setState` variables outside the `useState` hook and
simply assigning them on the initial render of the component.

Give that a try!

<callout-success>
The button will finally work on this one, I promise!
</callout-success>
3 changes: 3 additions & 0 deletions exercises/01.use-state/04.solution.preserve-state/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
# Preserve State

👨‍💼 Great work! Our UI is working properly! By preserving our state we're able to
make changes to it and render again whenever that value changes.
6 changes: 6 additions & 0 deletions exercises/01.use-state/FINISHED.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
# useState

👨‍💼 This is a great time for you to take a break and reflect on your learnings so
far. Take a moment and then when you're ready, we'll see you in the next
exercise.

There's still plenty to do!
30 changes: 30 additions & 0 deletions exercises/02.multiple-hooks/01.problem.phase/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,31 @@
# Render Phase

🧝‍♂️ Hi! I made a change to the code a bit. Now we're rendering two buttons, the
count button is still there, but now we're also a button for disabling the count
button. I needed to add another `useState` for that, but it's not working. You
can <PrevDiffLink>check my work</PrevDiffLink> if you'd like. Can you get it
working? Thanks!

👨‍💼 Thanks for adding those buttons Kellie!

Ok, so what we need you to do is fix the problem. If you add a
`console.log({ count, enabled })` to the component, you'll get
`{ count: 0, enabled: 0 }`. This is because the first time the `useState` is
called initializes the state and the second time it's called, it just references
the first one.

So to start off fixing this issue, you're going to need to formalize how we
determine whether state gets initialized or referenced.

Really, `useState` can be called in two scenarios:

- Initialization
- Updates

So we're going to keep track of how this is called with a `phase` variable. The
emoji will guide you in the right direction. Good luck!

<callout-warning>
Note it's not going to quite work when you're finished with this step, but
it'll work soon!
</callout-warning>
7 changes: 7 additions & 0 deletions exercises/02.multiple-hooks/01.solution.phase/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
# Render Phase

👨‍💼 Great! It's not quite working yet, now if we add
`console.log({ count, enabled })` to the component, we'll get
`{ count: 0, enabled: true }` like you'd expect, but when you click the counter
button we get `{ count: 1, enabled: 1 }` 😅. And if you click the disable button
you get `{ count: false, enabled: false }`. What the heck is going on!? Let's
find out.
17 changes: 17 additions & 0 deletions exercises/02.multiple-hooks/02.problem.hook-id/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,18 @@
# Hook ID

👨‍💼 Based on what's happening now, I think we're not isolating the `state`
between the two hooks. We need to uniquely identify each hook and store their
state separately.

We know that the hooks are called in the same order every time, so we could keep
a call index (we'll call it the `hookIndex`) and increment it every time
`useState` is called. That way, we could assign the first hook an ID of `0` and
the second hook an ID of `1`.

Then we store the `state` and `setState` in an array with their ID as the
key.

Then, whenever we render we just reset the `hookIndex` to `0` and we'll be
golden!

<callout-success>It'll work this time, I promise!</callout-success>
4 changes: 4 additions & 0 deletions exercises/02.multiple-hooks/02.solution.hook-id/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
# Hook ID

👨‍💼 Hey, that works! And now you understand why it's important to avoid
conditionally calling hooks or call them in loops. Their call order is their
only uniquely identifying trait!
4 changes: 4 additions & 0 deletions exercises/02.multiple-hooks/FINISHED.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
# Multiple Hooks

👨‍💼 Whew, now we've got multiple `useState` hooks in a single component working!
It's a good time for a break. Write down what you learned, then we'll see you
back soon!
36 changes: 31 additions & 5 deletions exercises/02.multiple-hooks/README.mdx
Original file line number Diff line number Diff line change
@@ -1,10 +1,36 @@
# Multiple Hooks

Often components require more than a single hook. In fact, often they'll use
two or more of the `useState` hook. If we tried that with our implementation
right now things wouldn't work so well and I think you can imagine why.

The tricky part about this is when you have more than one hook, you need to be
able to track their values over the lifetime of the component relative to each
other and that component. What makes this difficult though is that there's no
uniquely identifying information about the hooks:

```tsx
const [count1, setCount1] = useState(0)
const [count2, setCount2] = useState(0)
```

Having two elements of state like this in a component is perfectly legitimate,
but our current implementation wouldn't work for that at all because the state
would be shared between the two hooks.

So how do we get around that?

Well, it's not entirely true to say that there's no uniquely identifying
information about the hooks.... There actually is something unique about these
function calls and that is the order in which they are called!

If we can assume that they'll always be called in the same order, then we can
assign the first one an ID of `0` and the second one an ID of `1`. Then we can
use that ID to track the state of the hooks!

Something you will hopefully gather from this exercise is an understanding of
why the ["rules of hooks"](https://react.dev/reference/rules/rules-of-hooks) is
a thing. The rules are:

- Only call Hooks at the top level
- Only call Hooks from React functions
a thing. Specifically the rule that hooks must be called at the top level (and
not conditionally).

As we work to implement th
So, let's get into it!
24 changes: 24 additions & 0 deletions exercises/03.use-effect/01.problem.callback/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,25 @@
# Callback

🧝‍♂️ I've added a `useEffect`, but it's not supported yet so the app's busted:

```tsx
useEffect(() => {
console.log('consider yourself effective!')
})
```

You can <PrevDiffLink>check my work</PrevDiffLink> if you'd like. Can you fix
it? Thanks!

👨‍💼 Sure thing Kellie, thanks!

Ok, so what we need to do here is going to feel a little familiar. We'll want to
create an `effects` array that stores all the callbacks. Then our `useEffect`
hook implementation will actually be pretty darn simple: get the `ID` for our
hook, add the callback to the `effects` array. We don't even have to return
anything.

The tricky bit will be to make the `appRoot.render` call synchronous with
`flushSync` so we can iterate through all the effects to call the callback.

I think you can do it. Let's go!
2 changes: 2 additions & 0 deletions exercises/03.use-effect/01.solution.callback/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
# Callback

👨‍💼 Great job! Now, can you handle the dependency array?
23 changes: 23 additions & 0 deletions exercises/03.use-effect/02.problem.dependencies/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,24 @@
# Dependencies

🧝‍♂️ I've updated the `useEffect`:

```tsx
useEffect(() => {
if (enabled) {
console.log('consider yourself effective!')
} else {
console.log('consider yourself ineffective!')
}
}, [enabled])
```

You can <PrevDiffLink>check my work</PrevDiffLink> if you'd like. The app's not
technically broken, but the logs should only happen when toggling enabled and
right now we're getting logs when clicking the counter as well.

👨‍💼 We can handle this! So you'll need to also keep track of the deps array and
even the previous value of the deps array. Then just add a little logic to
determine whether to call the effect callback based on whether any of the
dependencies changed.

You can do this. Let's go!
2 changes: 2 additions & 0 deletions exercises/03.use-effect/02.solution.dependencies/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
# Dependencies

👨‍💼 Great work! Now the `useEffect` dependencies work. Well done 👏
7 changes: 7 additions & 0 deletions exercises/03.use-effect/FINISHED.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
# useEffect

👨‍💼 That's as far as we're going to take `useEffect`. Unfortunately there's just
not a good way to handle the cleanup function since we don't have a way to track
when a component gets added and removed from the page because the API React
offers us for that is `useEffect` 😅

But hopefully you learned something valuable here! Write down what you learned!
22 changes: 22 additions & 0 deletions exercises/03.use-effect/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,23 @@
# useEffect

The `useEffect` hook has a simple API:

```tsx
useEffect(callback, [dep1, dep2])
```

The dependencies are optional, the callback can return a cleanup function. On
re-renders that change the dependencies, the callback will be called again (
after first calling the previous cleanup if one was given). If no dependency
array is provided, the callback will be called on every render. That's about it.

We can follow the same pattern with storing the callback and dependencies as we
did with `useState` before. And we can call the callbacks in the `render`
function we have. Should be pretty simple!

However, because we're not React, we don't actually know when the component has
finished rendering. So we're going to use React's
[`flushSync`](https://react.dev/reference/react-dom/flushSync) API to force the
render to happen synchronously so that we can get the callbacks to run.

So let's get into it!
10 changes: 10 additions & 0 deletions exercises/FINISHED.mdx
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# Build React Hooks 🪝

Hooray! You're all done! 👏👏

Of course there's much more we can do for our implementations to make them more
like the actual built in version of these hooks, but I think we've gone far
enough for you to understand how hooks work behind the scenes a bit.

If you want to explore the actual implementation of hooks in the source code,
[start here](https://github.com/facebook/react/blob/e02baf6c92833a0d45a77fb2e741676f393c24f7/packages/react-reconciler/src/ReactFiberHooks.js#L3837-L3964).
It's pretty interesting (and certainly more complicated), but it's not entirely
dissimilar to what we've implemented. Hopefully this helps you understand how
React hooks work under the hood!
13 changes: 13 additions & 0 deletions exercises/README.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Build React Hooks 🪝

👨‍💼 Hello, my name is Peter the Product Manager. I'm here to help you get
oriented and to give you your assignments for the workshop!

We're going to build our own implementation of React hooks so you get a deeper
understanding of how they work behind the scenes. We'll be building `useState`
and `useEffect`.

<callout-warning>
This will of course not be as rigerous as the official implementation.
</callout-warning>

You're gonna rock with this! Let's get going!

🦺 One note, we're going to be doing some fun hackery around here so we'll be
abusing TypeScript a bit. Feel free to throw around `any` and
`// @ts-expect-error` here and there.

0 comments on commit 7376aa5

Please sign in to comment.