From 7dca270be544f3febd419938d6c9566fdb5178f3 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Fri, 28 Jun 2024 14:46:15 -0600 Subject: [PATCH] finish exercises and in-code instructions --- .../01.problem.initial-state/index.tsx | 3 +- .../01.solution.initial-state/index.tsx | 5 +- .../02.problem.update-state/index.tsx | 7 +- .../03.problem.re-render/index.tsx | 5 ++ .../03.solution.re-render/index.tsx | 12 ++- .../04.problem.preserve-state/index.tsx | 21 +++++- .../04.solution.preserve-state/index.tsx | 21 +++++- .../01.problem.phase/index.tsx | 41 ++++++++-- .../01.solution.phase/index.tsx | 35 +++++++-- .../02.problem.hook-id/index.tsx | 45 +++++++++-- .../02.solution.hook-id/index.tsx | 39 ++++++++-- .../01.problem.callback/index.tsx | 58 +++++++++++++-- .../01.solution.callback/index.tsx | 61 +++++++++++++-- .../02.problem.dependencies/index.tsx | 74 +++++++++++++++++-- .../02.solution.dependencies/index.tsx | 8 +- 15 files changed, 383 insertions(+), 52 deletions(-) diff --git a/exercises/01.use-state/01.problem.initial-state/index.tsx b/exercises/01.use-state/01.problem.initial-state/index.tsx index eca15fc..c9ad77c 100644 --- a/exercises/01.use-state/01.problem.initial-state/index.tsx +++ b/exercises/01.use-state/01.problem.initial-state/index.tsx @@ -1,13 +1,14 @@ import { createRoot } from 'react-dom/client' // 🐨 create a `useState` function which accepts the initial state and returns -// an array of the state and a function to update it. +// an array of the state and a no-op function: () => {} // 🦺 note you may need to ignore some typescript errors here. We'll fix them later. // Feel free to make the `useState` a generic though! function Counter() { // @ts-expect-error 💣 delete this comment const [count, setCount] = useState(0) + // 🦺 you'll get an error for this we'll fix that next const increment = () => setCount(count + 1) return (
diff --git a/exercises/01.use-state/01.solution.initial-state/index.tsx b/exercises/01.use-state/01.solution.initial-state/index.tsx index bbd08ea..864ead0 100644 --- a/exercises/01.use-state/01.solution.initial-state/index.tsx +++ b/exercises/01.use-state/01.solution.initial-state/index.tsx @@ -1,13 +1,14 @@ import { createRoot } from 'react-dom/client' function useState(initialState: State) { - let state = initialState - const setState = (newState: State) => (state = newState) + const state = initialState + const setState = () => {} return [state, setState] as const } function Counter() { const [count, setCount] = useState(0) + // @ts-expect-error we'll fix this soon const increment = () => setCount(count + 1) return (
diff --git a/exercises/01.use-state/02.problem.update-state/index.tsx b/exercises/01.use-state/02.problem.update-state/index.tsx index bbd08ea..86166b3 100644 --- a/exercises/01.use-state/02.problem.update-state/index.tsx +++ b/exercises/01.use-state/02.problem.update-state/index.tsx @@ -1,13 +1,16 @@ import { createRoot } from 'react-dom/client' function useState(initialState: State) { - let state = initialState - const setState = (newState: State) => (state = newState) + // 🐨 change this to let + const state = initialState + // 🐨 update this to accept newState and assign state to that + const setState = () => {} return [state, setState] as const } function Counter() { const [count, setCount] = useState(0) + // @ts-expect-error 💣 delete this comment const increment = () => setCount(count + 1) return (
diff --git a/exercises/01.use-state/03.problem.re-render/index.tsx b/exercises/01.use-state/03.problem.re-render/index.tsx index bbd08ea..4ac6313 100644 --- a/exercises/01.use-state/03.problem.re-render/index.tsx +++ b/exercises/01.use-state/03.problem.re-render/index.tsx @@ -2,6 +2,7 @@ import { createRoot } from 'react-dom/client' function useState(initialState: State) { let state = initialState + // 🐨 update this function to call render after setting the state const setState = (newState: State) => (state = newState) return [state, setState] as const } @@ -19,4 +20,8 @@ function Counter() { const rootEl = document.createElement('div') document.body.append(rootEl) const appRoot = createRoot(rootEl) + +// 🐨 place this in a new function called render appRoot.render() + +// 🐨 call render here to kick things off diff --git a/exercises/01.use-state/03.solution.re-render/index.tsx b/exercises/01.use-state/03.solution.re-render/index.tsx index bbd08ea..0e55fdf 100644 --- a/exercises/01.use-state/03.solution.re-render/index.tsx +++ b/exercises/01.use-state/03.solution.re-render/index.tsx @@ -2,7 +2,10 @@ import { createRoot } from 'react-dom/client' function useState(initialState: State) { let state = initialState - const setState = (newState: State) => (state = newState) + const setState = (newState: State) => { + state = newState + render() + } return [state, setState] as const } @@ -19,4 +22,9 @@ function Counter() { const rootEl = document.createElement('div') document.body.append(rootEl) const appRoot = createRoot(rootEl) -appRoot.render() + +function render() { + appRoot.render() +} + +render() diff --git a/exercises/01.use-state/04.problem.preserve-state/index.tsx b/exercises/01.use-state/04.problem.preserve-state/index.tsx index bbd08ea..a583d49 100644 --- a/exercises/01.use-state/04.problem.preserve-state/index.tsx +++ b/exercises/01.use-state/04.problem.preserve-state/index.tsx @@ -1,8 +1,20 @@ import { createRoot } from 'react-dom/client' +// 🐨 create state and setState variables here using let +// 🦺 set their type to "any" + function useState(initialState: State) { + // 🐨 remove the "let" and "const" here so this function references the + // variables declared above + // 🐨 Next, change this so we only do these assignments if the state is undefined let state = initialState - const setState = (newState: State) => (state = newState) + const setState = (newState: State) => { + state = newState + render() + } + // 🦺 because our state and setState are now typed as any, you may choose to + // update this to as [State, (newState: State) => void] so we can preserve + // the type of state return [state, setState] as const } @@ -19,4 +31,9 @@ function Counter() { const rootEl = document.createElement('div') document.body.append(rootEl) const appRoot = createRoot(rootEl) -appRoot.render() + +function render() { + appRoot.render() +} + +render() diff --git a/exercises/01.use-state/04.solution.preserve-state/index.tsx b/exercises/01.use-state/04.solution.preserve-state/index.tsx index bbd08ea..8a4b676 100644 --- a/exercises/01.use-state/04.solution.preserve-state/index.tsx +++ b/exercises/01.use-state/04.solution.preserve-state/index.tsx @@ -1,14 +1,22 @@ import { createRoot } from 'react-dom/client' +let state: any, setState: any + function useState(initialState: State) { - let state = initialState - const setState = (newState: State) => (state = newState) - return [state, setState] as const + if (state === undefined) { + state = initialState + setState = (newState: State) => { + state = newState + render() + } + } + return [state, setState] as [State, (newState: State) => void] } function Counter() { const [count, setCount] = useState(0) const increment = () => setCount(count + 1) + return (
@@ -19,4 +27,9 @@ function Counter() { const rootEl = document.createElement('div') document.body.append(rootEl) const appRoot = createRoot(rootEl) -appRoot.render() + +function render() { + appRoot.render() +} + +render() diff --git a/exercises/02.multiple-hooks/01.problem.phase/index.tsx b/exercises/02.multiple-hooks/01.problem.phase/index.tsx index bbd08ea..8bb5ce6 100644 --- a/exercises/02.multiple-hooks/01.problem.phase/index.tsx +++ b/exercises/02.multiple-hooks/01.problem.phase/index.tsx @@ -1,17 +1,40 @@ import { createRoot } from 'react-dom/client' +// 🐨 create two Symbols for the phase: "INITIALIZATION" and "UPDATE" +// 💯 as extra credit, give them a descriptive name + +// 🦺 create a type called Phase which is the typeof INITIALIZATION | typeof UPDATE + +// 🐨 create a variable called phase of type Phase and set it to INITIALIZATION + +let state: any, setState: any + function useState(initialState: State) { - let state = initialState - const setState = (newState: State) => (state = newState) - return [state, setState] as const + // 🐨 change this to check whether the phase is INITIALIZATION + if (state === undefined) { + state = initialState + setState = (newState: State) => { + state = newState + // 🐨 pass the UPDATE phase to render here + render() + } + } + return [state, setState] as [State, (newState: State) => void] } function Counter() { const [count, setCount] = useState(0) const increment = () => setCount(count + 1) + + const [enabled, setEnabled] = useState(true) + const toggle = () => setEnabled(!enabled) + return (
- + +
) } @@ -19,4 +42,12 @@ function Counter() { const rootEl = document.createElement('div') document.body.append(rootEl) const appRoot = createRoot(rootEl) -appRoot.render() + +// 🐨 accept a newPhase argument +function render() { + // 🐨 assign the phase to the newPhase + appRoot.render() +} + +// 🐨 call this with the INITIALIZATION phase +render() diff --git a/exercises/02.multiple-hooks/01.solution.phase/index.tsx b/exercises/02.multiple-hooks/01.solution.phase/index.tsx index bbd08ea..046a77a 100644 --- a/exercises/02.multiple-hooks/01.solution.phase/index.tsx +++ b/exercises/02.multiple-hooks/01.solution.phase/index.tsx @@ -1,17 +1,36 @@ import { createRoot } from 'react-dom/client' +const INITIALIZATION = Symbol('phase.initialization') +const UPDATE = Symbol('phase.update') +type Phase = typeof INITIALIZATION | typeof UPDATE +let phase: Phase + +let state: any, setState: any + function useState(initialState: State) { - let state = initialState - const setState = (newState: State) => (state = newState) - return [state, setState] as const + if (phase === INITIALIZATION) { + state = initialState + setState = (newState: State) => { + state = newState + render(UPDATE) + } + } + return [state, setState] as [State, (newState: State) => void] } function Counter() { const [count, setCount] = useState(0) const increment = () => setCount(count + 1) + + const [enabled, setEnabled] = useState(true) + const toggle = () => setEnabled(!enabled) + return (
- + +
) } @@ -19,4 +38,10 @@ function Counter() { const rootEl = document.createElement('div') document.body.append(rootEl) const appRoot = createRoot(rootEl) -appRoot.render() + +function render(newPhase: Phase) { + phase = newPhase + appRoot.render() +} + +render(INITIALIZATION) diff --git a/exercises/02.multiple-hooks/02.problem.hook-id/index.tsx b/exercises/02.multiple-hooks/02.problem.hook-id/index.tsx index bbd08ea..282d867 100644 --- a/exercises/02.multiple-hooks/02.problem.hook-id/index.tsx +++ b/exercises/02.multiple-hooks/02.problem.hook-id/index.tsx @@ -1,17 +1,45 @@ import { createRoot } from 'react-dom/client' +const INITIALIZATION = Symbol('phase.initialization') +const UPDATE = Symbol('phase.update') +type Phase = typeof INITIALIZATION | typeof UPDATE +let phase: Phase +// 🐨 make a hookIndex variable here that starts at 0 +// 🐨 make a variable called "states" which is an array of arrays (one for each +// return value of a useState call) + +// 💣 delete these variable declarations +let state: any, setState: any + function useState(initialState: State) { - let state = initialState - const setState = (newState: State) => (state = newState) - return [state, setState] as const + // 🐨 create a variable called "id" and assign it to "hookIndex++" + if (phase === INITIALIZATION) { + // 🐨 assign states[id] to an array with the initialState and the setState function + // rather than assigning the values to the old variables + state = initialState + setState = (newState: State) => { + // 🐨 instead of reassigning the variable state to the newState, update states[id][0] to it. + state = newState + render(UPDATE) + } + } + // 🐨 return the value at states[id] instead of the old variables + return [state, setState] as [State, (newState: State) => void] } function Counter() { const [count, setCount] = useState(0) const increment = () => setCount(count + 1) + + const [enabled, setEnabled] = useState(true) + const toggle = () => setEnabled(!enabled) + return (
- + +
) } @@ -19,4 +47,11 @@ function Counter() { const rootEl = document.createElement('div') document.body.append(rootEl) const appRoot = createRoot(rootEl) -appRoot.render() + +function render(newPhase: Phase) { + // 🐨 set the hookIndex to 0 + phase = newPhase + appRoot.render() +} + +render(INITIALIZATION) diff --git a/exercises/02.multiple-hooks/02.solution.hook-id/index.tsx b/exercises/02.multiple-hooks/02.solution.hook-id/index.tsx index bbd08ea..2d3c01a 100644 --- a/exercises/02.multiple-hooks/02.solution.hook-id/index.tsx +++ b/exercises/02.multiple-hooks/02.solution.hook-id/index.tsx @@ -1,17 +1,39 @@ import { createRoot } from 'react-dom/client' +const INITIALIZATION = Symbol('phase.initialization') +const UPDATE = Symbol('phase.update') +type Phase = typeof INITIALIZATION | typeof UPDATE +let phase: Phase +let hookIndex = 0 +const states: Array<[any, (newState: any) => void]> = [] + function useState(initialState: State) { - let state = initialState - const setState = (newState: State) => (state = newState) - return [state, setState] as const + const id = hookIndex++ + if (phase === INITIALIZATION) { + states[id] = [ + initialState, + (newState: State) => { + states[id][0] = newState + render(UPDATE) + }, + ] + } + return states[id] as [State, (newState: State) => void] } function Counter() { const [count, setCount] = useState(0) const increment = () => setCount(count + 1) + + const [enabled, setEnabled] = useState(true) + const toggle = () => setEnabled(!enabled) + return (
- + +
) } @@ -19,4 +41,11 @@ function Counter() { const rootEl = document.createElement('div') document.body.append(rootEl) const appRoot = createRoot(rootEl) -appRoot.render() + +function render(newPhase: Phase) { + hookIndex = 0 + phase = newPhase + appRoot.render() +} + +render(INITIALIZATION) diff --git a/exercises/03.use-effect/01.problem.callback/index.tsx b/exercises/03.use-effect/01.problem.callback/index.tsx index bbd08ea..95cba28 100644 --- a/exercises/03.use-effect/01.problem.callback/index.tsx +++ b/exercises/03.use-effect/01.problem.callback/index.tsx @@ -1,17 +1,51 @@ +// 💰 you'll need this +// import { flushSync } from 'react-dom' import { createRoot } from 'react-dom/client' +const INITIALIZATION = Symbol('phase.initialization') +const UPDATE = Symbol('phase.update') +type Phase = typeof INITIALIZATION | typeof UPDATE +let phase: Phase +let hookIndex = 0 +const states: Array<[any, (newState: any) => void]> = [] +type EffectCallback = () => void +// 🐨 make a variable called "effects" that's an array of objects with a callback property + function useState(initialState: State) { - let state = initialState - const setState = (newState: State) => (state = newState) - return [state, setState] as const + const id = hookIndex++ + if (phase === INITIALIZATION) { + states[id] = [ + initialState, + (newState: State) => { + states[id][0] = newState + render(UPDATE) + }, + ] + } + return states[id] as [State, (newState: State) => void] } +// 🐨 create a useEffect function here that accepts a callback, +// gets the id from hookIndex++, and adds it to effects + function Counter() { const [count, setCount] = useState(0) const increment = () => setCount(count + 1) + + const [enabled, setEnabled] = useState(true) + const toggle = () => setEnabled(!enabled) + + // @ts-expect-error 💣 delete this comment + useEffect(() => { + console.log('consider yourself effective!') + }) + return (
- + +
) } @@ -19,4 +53,18 @@ function Counter() { const rootEl = document.createElement('div') document.body.append(rootEl) const appRoot = createRoot(rootEl) -appRoot.render() + +function render(newPhase: Phase) { + hookIndex = 0 + phase = newPhase + + // 🦉 Because we have no way of knowing when React will finish rendering so we + // can call our effects, we need to cheat a little bit by telling React to + // render synchronously instead... + // 🐨 wrap this in flushSync + appRoot.render() + + // 🐨 add a for of loop for all the effects and call their callbacks. +} + +render(INITIALIZATION) diff --git a/exercises/03.use-effect/01.solution.callback/index.tsx b/exercises/03.use-effect/01.solution.callback/index.tsx index bbd08ea..6af7f93 100644 --- a/exercises/03.use-effect/01.solution.callback/index.tsx +++ b/exercises/03.use-effect/01.solution.callback/index.tsx @@ -1,17 +1,53 @@ +import { flushSync } from 'react-dom' import { createRoot } from 'react-dom/client' +const INITIALIZATION = Symbol('phase.initialization') +const UPDATE = Symbol('phase.update') +type Phase = typeof INITIALIZATION | typeof UPDATE +let phase: Phase +let hookIndex = 0 +const states: Array<[any, (newState: any) => void]> = [] +type EffectCallback = () => void +const effects: Array<{ + callback: EffectCallback +}> = [] + function useState(initialState: State) { - let state = initialState - const setState = (newState: State) => (state = newState) - return [state, setState] as const + const id = hookIndex++ + if (phase === INITIALIZATION) { + states[id] = [ + initialState, + (newState: State) => { + states[id][0] = newState + render(UPDATE) + }, + ] + } + return states[id] as [State, (newState: State) => void] +} + +function useEffect(callback: EffectCallback) { + const id = hookIndex++ + effects[id] = { callback } } function Counter() { const [count, setCount] = useState(0) const increment = () => setCount(count + 1) + + const [enabled, setEnabled] = useState(true) + const toggle = () => setEnabled(!enabled) + + useEffect(() => { + console.log('consider yourself effective!') + }) + return (
- + +
) } @@ -19,4 +55,19 @@ function Counter() { const rootEl = document.createElement('div') document.body.append(rootEl) const appRoot = createRoot(rootEl) -appRoot.render() + +function render(newPhase: Phase) { + hookIndex = 0 + phase = newPhase + flushSync(() => { + appRoot.render() + }) + + for (const effect of effects) { + if (!effect) continue + + effect.callback() + } +} + +render(INITIALIZATION) diff --git a/exercises/03.use-effect/02.problem.dependencies/index.tsx b/exercises/03.use-effect/02.problem.dependencies/index.tsx index bbd08ea..1e2100b 100644 --- a/exercises/03.use-effect/02.problem.dependencies/index.tsx +++ b/exercises/03.use-effect/02.problem.dependencies/index.tsx @@ -1,17 +1,61 @@ +import { flushSync } from 'react-dom' import { createRoot } from 'react-dom/client' +const INITIALIZATION = Symbol('phase.initialization') +const UPDATE = Symbol('phase.update') +type Phase = typeof INITIALIZATION | typeof UPDATE +let phase: Phase +let hookIndex = 0 +const states: Array<[any, (newState: any) => void]> = [] +type EffectCallback = () => void +const effects: Array<{ + callback: EffectCallback + // 🦺 add an optional deps and prevDeps properties which can be arrays of anything +}> = [] + function useState(initialState: State) { - let state = initialState - const setState = (newState: State) => (state = newState) - return [state, setState] as const + const id = hookIndex++ + if (phase === INITIALIZATION) { + states[id] = [ + initialState, + (newState: State) => { + states[id][0] = newState + render(UPDATE) + }, + ] + } + return states[id] as [State, (newState: State) => void] +} + +// 🐨 add an optional deps argument here +function useEffect(callback: EffectCallback) { + const id = hookIndex++ + // 🐨 add deps to this object and prevDeps should be effects[id]?.deps + effects[id] = { callback } } function Counter() { const [count, setCount] = useState(0) const increment = () => setCount(count + 1) + + const [enabled, setEnabled] = useState(true) + const toggle = () => setEnabled(!enabled) + + useEffect(() => { + if (enabled) { + console.log('consider yourself effective!') + } else { + console.log('consider yourself ineffective!') + } + // @ts-expect-error 💣 delete this comment + }, [enabled]) + return (
- + +
) } @@ -19,4 +63,24 @@ function Counter() { const rootEl = document.createElement('div') document.body.append(rootEl) const appRoot = createRoot(rootEl) -appRoot.render() + +function render(newPhase: Phase) { + hookIndex = 0 + phase = newPhase + flushSync(() => { + appRoot.render() + }) + + for (const effect of effects) { + if (!effect) continue + + // 🐨 create a hasDepsChanged variable to determine whether the effect should be called + // if the effect has no deps, hasDepsChanged should be true + // if the effect does have deps, calculate whether any item in the dep array + // is different from the corresponding item in the prevDeps array + + effect.callback() + } +} + +render(INITIALIZATION) diff --git a/exercises/03.use-effect/02.solution.dependencies/index.tsx b/exercises/03.use-effect/02.solution.dependencies/index.tsx index ca54047..92d8c7c 100644 --- a/exercises/03.use-effect/02.solution.dependencies/index.tsx +++ b/exercises/03.use-effect/02.solution.dependencies/index.tsx @@ -4,13 +4,13 @@ import { createRoot } from 'react-dom/client' const INITIALIZATION = Symbol('phase.initialization') const UPDATE = Symbol('phase.update') type Phase = typeof INITIALIZATION | typeof UPDATE -let phase: Phase = INITIALIZATION +let phase: Phase let hookIndex = 0 const states: Array<[any, (newState: any) => void]> = [] type EffectCallback = () => void const effects: Array<{ callback: EffectCallback - deps: Array + deps?: Array prevDeps?: Array }> = [] @@ -28,7 +28,7 @@ function useState(initialState: State) { return states[id] as [State, (newState: State) => void] } -function useEffect(callback: EffectCallback, deps: Array) { +function useEffect(callback: EffectCallback, deps?: Array) { const id = hookIndex++ effects[id] = { callback, deps, prevDeps: effects[id]?.deps } } @@ -49,7 +49,7 @@ function Counter() { }, [enabled]) return ( -
+