diff --git a/exercises/01.use-state/01.solution.initial-state/README.mdx b/exercises/01.use-state/01.solution.initial-state/README.mdx index 0917c31..4dc0803 100644 --- a/exercises/01.use-state/01.solution.initial-state/README.mdx +++ b/exercises/01.use-state/01.solution.initial-state/README.mdx @@ -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. diff --git a/exercises/01.use-state/02.problem.update-state/README.mdx b/exercises/01.use-state/02.problem.update-state/README.mdx index ba173fd..a43e616 100644 --- a/exercises/01.use-state/02.problem.update-state/README.mdx +++ b/exercises/01.use-state/02.problem.update-state/README.mdx @@ -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. + + + When you do this, it'll not actually update the number in the button either. + We'll get to that soon! + diff --git a/exercises/01.use-state/02.solution.update-state/README.mdx b/exercises/01.use-state/02.solution.update-state/README.mdx index ba173fd..2f33ae5 100644 --- a/exercises/01.use-state/02.solution.update-state/README.mdx +++ b/exercises/01.use-state/02.solution.update-state/README.mdx @@ -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?! diff --git a/exercises/01.use-state/03.problem.re-render/README.mdx b/exercises/01.use-state/03.problem.re-render/README.mdx index 7ebea2b..83d9b32 100644 --- a/exercises/01.use-state/03.problem.re-render/README.mdx +++ b/exercises/01.use-state/03.problem.re-render/README.mdx @@ -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() +``` + +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. + + + When you're finished with this, the UI will **still** not work like you + expect. I promise we'll get to that very soon! + diff --git a/exercises/01.use-state/03.solution.re-render/README.mdx b/exercises/01.use-state/03.solution.re-render/README.mdx index 7ebea2b..247f3df 100644 --- a/exercises/01.use-state/03.solution.re-render/README.mdx +++ b/exercises/01.use-state/03.solution.re-render/README.mdx @@ -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. diff --git a/exercises/01.use-state/04.problem.preserve-state/README.mdx b/exercises/01.use-state/04.problem.preserve-state/README.mdx index 48be07e..deb434c 100644 --- a/exercises/01.use-state/04.problem.preserve-state/README.mdx +++ b/exercises/01.use-state/04.problem.preserve-state/README.mdx @@ -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 + + + 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. + + +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! + + + The button will finally work on this one, I promise! + diff --git a/exercises/01.use-state/04.solution.preserve-state/README.mdx b/exercises/01.use-state/04.solution.preserve-state/README.mdx index 48be07e..9c2fb51 100644 --- a/exercises/01.use-state/04.solution.preserve-state/README.mdx +++ b/exercises/01.use-state/04.solution.preserve-state/README.mdx @@ -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. diff --git a/exercises/01.use-state/FINISHED.mdx b/exercises/01.use-state/FINISHED.mdx index 2f1904c..60269b3 100644 --- a/exercises/01.use-state/FINISHED.mdx +++ b/exercises/01.use-state/FINISHED.mdx @@ -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! diff --git a/exercises/02.multiple-hooks/01.problem.phase/README.mdx b/exercises/02.multiple-hooks/01.problem.phase/README.mdx index 4606169..551bf2b 100644 --- a/exercises/02.multiple-hooks/01.problem.phase/README.mdx +++ b/exercises/02.multiple-hooks/01.problem.phase/README.mdx @@ -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 check my work 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! + + + Note it's not going to quite work when you're finished with this step, but + it'll work soon! + diff --git a/exercises/02.multiple-hooks/01.solution.phase/README.mdx b/exercises/02.multiple-hooks/01.solution.phase/README.mdx index 4606169..499af69 100644 --- a/exercises/02.multiple-hooks/01.solution.phase/README.mdx +++ b/exercises/02.multiple-hooks/01.solution.phase/README.mdx @@ -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. diff --git a/exercises/02.multiple-hooks/02.problem.hook-id/README.mdx b/exercises/02.multiple-hooks/02.problem.hook-id/README.mdx index fcbe8c5..2d9e08f 100644 --- a/exercises/02.multiple-hooks/02.problem.hook-id/README.mdx +++ b/exercises/02.multiple-hooks/02.problem.hook-id/README.mdx @@ -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! + +It'll work this time, I promise! diff --git a/exercises/02.multiple-hooks/02.solution.hook-id/README.mdx b/exercises/02.multiple-hooks/02.solution.hook-id/README.mdx index fcbe8c5..02fd639 100644 --- a/exercises/02.multiple-hooks/02.solution.hook-id/README.mdx +++ b/exercises/02.multiple-hooks/02.solution.hook-id/README.mdx @@ -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! diff --git a/exercises/02.multiple-hooks/FINISHED.mdx b/exercises/02.multiple-hooks/FINISHED.mdx index 2ac69f7..8d6d98e 100644 --- a/exercises/02.multiple-hooks/FINISHED.mdx +++ b/exercises/02.multiple-hooks/FINISHED.mdx @@ -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! diff --git a/exercises/02.multiple-hooks/README.mdx b/exercises/02.multiple-hooks/README.mdx index 67aeb28..dd2a1b7 100644 --- a/exercises/02.multiple-hooks/README.mdx +++ b/exercises/02.multiple-hooks/README.mdx @@ -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! diff --git a/exercises/03.use-effect/01.problem.callback/README.mdx b/exercises/03.use-effect/01.problem.callback/README.mdx index 447a95f..ea2cc2c 100644 --- a/exercises/03.use-effect/01.problem.callback/README.mdx +++ b/exercises/03.use-effect/01.problem.callback/README.mdx @@ -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 check my work 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! diff --git a/exercises/03.use-effect/01.solution.callback/README.mdx b/exercises/03.use-effect/01.solution.callback/README.mdx index 447a95f..ca2d628 100644 --- a/exercises/03.use-effect/01.solution.callback/README.mdx +++ b/exercises/03.use-effect/01.solution.callback/README.mdx @@ -1 +1,3 @@ # Callback + +👨‍💼 Great job! Now, can you handle the dependency array? diff --git a/exercises/03.use-effect/02.problem.dependencies/README.mdx b/exercises/03.use-effect/02.problem.dependencies/README.mdx index 3c3f15c..f558db8 100644 --- a/exercises/03.use-effect/02.problem.dependencies/README.mdx +++ b/exercises/03.use-effect/02.problem.dependencies/README.mdx @@ -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 check my work 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! diff --git a/exercises/03.use-effect/02.solution.dependencies/README.mdx b/exercises/03.use-effect/02.solution.dependencies/README.mdx index 3c3f15c..48138e5 100644 --- a/exercises/03.use-effect/02.solution.dependencies/README.mdx +++ b/exercises/03.use-effect/02.solution.dependencies/README.mdx @@ -1 +1,3 @@ # Dependencies + +👨‍💼 Great work! Now the `useEffect` dependencies work. Well done 👏 diff --git a/exercises/03.use-effect/FINISHED.mdx b/exercises/03.use-effect/FINISHED.mdx index 412c328..aaa17be 100644 --- a/exercises/03.use-effect/FINISHED.mdx +++ b/exercises/03.use-effect/FINISHED.mdx @@ -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! diff --git a/exercises/03.use-effect/README.mdx b/exercises/03.use-effect/README.mdx index 412c328..db3db6a 100644 --- a/exercises/03.use-effect/README.mdx +++ b/exercises/03.use-effect/README.mdx @@ -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! diff --git a/exercises/FINISHED.mdx b/exercises/FINISHED.mdx index 076a863..a512a43 100644 --- a/exercises/FINISHED.mdx +++ b/exercises/FINISHED.mdx @@ -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! diff --git a/exercises/README.mdx b/exercises/README.mdx index ea8f544..1e8df43 100644 --- a/exercises/README.mdx +++ b/exercises/README.mdx @@ -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`. + + + This will of course not be as rigerous as the official implementation. + + +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.