diff --git a/src/__tests__/01.js b/src/__tests__/01.js index cba238a3..519f0c2c 100644 --- a/src/__tests__/01.js +++ b/src/__tests__/01.js @@ -2,8 +2,8 @@ import * as React from 'react' import {alfredTip} from '@kentcdodds/react-workshop-app/test-utils' import {render} from '@testing-library/react' import userEvent from '@testing-library/user-event' -import App from '../final/01' -// import App from '../exercise/01' +// import App from '../final/01' +import App from '../exercise/01' // don't do this in regular tests! const Counter = App().type diff --git a/src/__tests__/02.js b/src/__tests__/02.js index cbb4b9d3..2fdc86f1 100644 --- a/src/__tests__/02.js +++ b/src/__tests__/02.js @@ -2,8 +2,8 @@ import * as React from 'react' import {alfredTip} from '@kentcdodds/react-workshop-app/test-utils' import {render, screen} from '@testing-library/react' import userEvent from '@testing-library/user-event' -import App from '../final/02' -// import App from '../exercise/02' +// import App from '../final/02' +import App from '../exercise/02' beforeEach(() => { jest.spyOn(window, 'fetch') diff --git a/src/__tests__/03.extra-2.js b/src/__tests__/03.extra-2.js index 4e490a6a..727f94c0 100644 --- a/src/__tests__/03.extra-2.js +++ b/src/__tests__/03.extra-2.js @@ -2,8 +2,8 @@ import * as React from 'react' import {alfredTip} from '@kentcdodds/react-workshop-app/test-utils' import {render, screen} from '@testing-library/react' import userEvent from '@testing-library/user-event' -import App from '../final/03.extra-2' -// import App from '../exercise/03.extra-2' +// import App from '../final/03.extra-2' +import App from '../exercise/03.extra-2' beforeEach(() => { jest.spyOn(window, 'fetch') diff --git a/src/__tests__/03.js b/src/__tests__/03.js index a23d2e92..31e5655b 100644 --- a/src/__tests__/03.js +++ b/src/__tests__/03.js @@ -1,8 +1,8 @@ import * as React from 'react' import {render, screen} from '@testing-library/react' import userEvent from '@testing-library/user-event' -import App from '../final/03' -// import App from '../exercise/03' +// import App from '../final/03' +import App from '../exercise/03' test('clicking the button increments the count', async () => { render() diff --git a/src/exercise/01.extra-1.js b/src/exercise/01.extra-1.js new file mode 100644 index 00000000..3d55d9a9 --- /dev/null +++ b/src/exercise/01.extra-1.js @@ -0,0 +1,15 @@ +import * as React from 'react' + +function App() { + return +} + +const countReducer = (count, change) => count + change + +function Counter({initialCount = 0, step = 1}) { + const [count, setCount] = React.useReducer(countReducer, initialCount) + const increment = () => setCount(step) + return +} + +export default App diff --git a/src/exercise/01.extra-2.js b/src/exercise/01.extra-2.js new file mode 100644 index 00000000..f87db212 --- /dev/null +++ b/src/exercise/01.extra-2.js @@ -0,0 +1,17 @@ +import * as React from 'react' + +function App() { + return +} + +const countReducer = (state, newState) => ({...state, ...newState}) + +function Counter({initialCount = 0, step = 1}) { + const [count, setCount] = React.useReducer(countReducer, { + count: initialCount, + }) + const increment = () => setCount({count: count + step}) + return +} + +export default App diff --git a/src/exercise/01.extra-3.js b/src/exercise/01.extra-3.js new file mode 100644 index 00000000..542413c9 --- /dev/null +++ b/src/exercise/01.extra-3.js @@ -0,0 +1,23 @@ +import * as React from 'react' + +function App() { + return +} + +const countReducer = (state, action) => ({ + ...state, + ...(isFunction(action) ? action(state) : action), +}) + +const isFunction = value => typeof value === 'function' + +function Counter({initialCount = 0, step = 1}) { + const [count, setCount] = React.useReducer(countReducer, { + count: initialCount, + }) + const increment = () => + setCount(currentState => ({count: currentState.count + step})) + return +} + +export default App diff --git a/src/exercise/01.extra-4.js b/src/exercise/01.extra-4.js new file mode 100644 index 00000000..94a0030f --- /dev/null +++ b/src/exercise/01.extra-4.js @@ -0,0 +1,24 @@ +import * as React from 'react' + +function App() { + return +} + +const countReducer = (state, action) => { + switch (action.type) { + case 'increment': { + return {...state, count: state.count + action.step} + } + default: { + throw new Error(`Unsupported action type: ${action.type}`) + } + } +} + +function Counter({initialCount = 0, step = 1}) { + const [count, dispatch] = React.useReducer(countReducer, initialCount) + const increment = () => dispatch({type: 'increment', step}) + return +} + +export default App diff --git a/src/exercise/01.js b/src/exercise/01.js index 199784da..44ebee17 100644 --- a/src/exercise/01.js +++ b/src/exercise/01.js @@ -3,21 +3,18 @@ import * as React from 'react' -function Counter({initialCount = 0, step = 1}) { - // 🐨 replace React.useState with React.useReducer. - // 💰 React.useReducer(countReducer, initialCount) - const [count, setCount] = React.useState(initialCount) +function App() { + return +} - // 💰 you can write the countReducer function so you don't have to make any - // changes to the next two lines of code! Remember: - // The 1st argument is called "state" - the current value of count - // The 2nd argument is called "newState" - the value passed to setCount - const increment = () => setCount(count + step) - return +const countReducer = (state, newState) => { + return newState } -function App() { - return +function Counter({initialCount = 0, step = 1}) { + const [count, setCount] = React.useReducer(countReducer, initialCount) + const increment = () => setCount(count + step) + return } export default App diff --git a/src/exercise/01.md b/src/exercise/01.md index 40973fe3..5b119405 100644 --- a/src/exercise/01.md +++ b/src/exercise/01.md @@ -214,4 +214,4 @@ important. ## 🦉 Feedback Fill out -[the feedback form](https://ws.kcd.im/?ws=Advanced%20React%20Hooks%20%F0%9F%94%A5&e=01%3A%20useReducer%3A%20simple%20Counter&em=). +[the feedback form](https://ws.kcd.im/?ws=Advanced%20React%20Hooks%20%F0%9F%94%A5&e=01%3A%20useReducer%3A%20simple%20Counter&em=elvinciqueira%40gmail.com). diff --git a/src/exercise/02.extra-1.js b/src/exercise/02.extra-1.js new file mode 100644 index 00000000..67d41ada --- /dev/null +++ b/src/exercise/02.extra-1.js @@ -0,0 +1,118 @@ +import * as React from 'react' +import { + fetchPokemon, + PokemonForm, + PokemonDataView, + PokemonInfoFallback, + PokemonErrorBoundary, +} from '../pokemon' + +function asyncReducer(state, action) { + switch (action.type) { + case 'pending': { + return {status: 'pending', data: null, error: null} + } + case 'resolved': { + return {status: 'resolved', data: action.data, error: null} + } + case 'rejected': { + return {status: 'rejected', data: null, error: action.error} + } + default: { + throw new Error(`Unhandled action type: ${action.type}`) + } + } +} + +function useAsync(asyncCallback, initialState) { + const [state, dispatch] = React.useReducer(asyncReducer, { + status: 'idle', + data: null, + error: null, + ...initialState, + }) + + React.useEffect(() => { + const promise = asyncCallback() + if (!promise) return + dispatch({type: 'pending'}) + promise.then( + data => dispatch({type: 'resolved', data}), + error => dispatch({type: 'rejected', error}), + ) + }, [asyncCallback]) + + return state +} + +function PokemonInfo({pokemonName}) { + const asyncCallback = React.useCallback(() => { + if (!pokemonName) return + return fetchPokemon(pokemonName) + }, [pokemonName]) + + const { + data: pokemon, + status, + error, + } = useAsync(asyncCallback, { + status: pokemonName ? 'pending' : 'idle', + }) + + switch (status) { + case 'idle': + return Submit a pokemon + case 'pending': + return + case 'rejected': + throw error + case 'resolved': + return + default: + throw new Error('This should be impossible') + } +} + +function App() { + const [pokemonName, setPokemonName] = React.useState('') + + function handleSubmit(newPokemonName) { + setPokemonName(newPokemonName) + } + + function handleReset() { + setPokemonName('') + } + + return ( +
+ +
+
+ + + +
+
+ ) +} + +function AppWithUnmountCheckbox() { + const [mountApp, setMountApp] = React.useState(true) + return ( +
+ +
+ {mountApp ? : null} +
+ ) +} + +export default AppWithUnmountCheckbox diff --git a/src/exercise/02.extra-2.js b/src/exercise/02.extra-2.js new file mode 100644 index 00000000..d9611276 --- /dev/null +++ b/src/exercise/02.extra-2.js @@ -0,0 +1,117 @@ +import * as React from 'react' +import { + fetchPokemon, + PokemonForm, + PokemonDataView, + PokemonInfoFallback, + PokemonErrorBoundary, +} from '../pokemon' + +function asyncReducer(state, action) { + switch (action.type) { + case 'pending': { + return {status: 'pending', data: null, error: null} + } + case 'resolved': { + return {status: 'resolved', data: action.data, error: null} + } + case 'rejected': { + return {status: 'rejected', data: null, error: action.error} + } + default: { + throw new Error(`Unhandled action type: ${action.type}`) + } + } +} + +function useAsync(initialState) { + const [state, dispatch] = React.useReducer(asyncReducer, { + status: 'idle', + data: null, + error: null, + ...initialState, + }) + const {data, error, status} = state + + const run = React.useCallback(promise => { + dispatch({type: 'pending'}) + promise.then( + data => dispatch({type: 'resolved', data}), + error => dispatch({type: 'rejected', error}), + ) + }, []) + + return {error, status, data, run} +} + +function PokemonInfo({pokemonName}) { + const { + data: pokemon, + status, + error, + run, + } = useAsync({status: pokemonName ? 'pending' : 'idle'}) + + React.useEffect(() => { + if (!pokemonName) return + const pokemonPromise = fetchPokemon(pokemonName) + run(pokemonPromise) + }, [pokemonName, run]) + + switch (status) { + case 'idle': + return Submit a pokemon + case 'pending': + return + case 'rejected': + throw error + case 'resolved': + return + default: + throw new Error('This should be impossible') + } +} + +function App() { + const [pokemonName, setPokemonName] = React.useState('') + + function handleSubmit(newPokemonName) { + setPokemonName(newPokemonName) + } + + function handleReset() { + setPokemonName('') + } + + return ( +
+ +
+
+ + + +
+
+ ) +} + +function AppWithUnmountCheckbox() { + const [mountApp, setMountApp] = React.useState(true) + return ( +
+ +
+ {mountApp ? : null} +
+ ) +} + +export default AppWithUnmountCheckbox diff --git a/src/exercise/02.extra-3.js b/src/exercise/02.extra-3.js new file mode 100644 index 00000000..2cd1630e --- /dev/null +++ b/src/exercise/02.extra-3.js @@ -0,0 +1,134 @@ +import * as React from 'react' +import { + fetchPokemon, + PokemonForm, + PokemonDataView, + PokemonInfoFallback, + PokemonErrorBoundary, +} from '../pokemon' + +function useSafeDispatch(dispatch) { + const mountedRef = React.useRef(false) + + React.useEffect(() => { + mountedRef.current = true + return () => (mountedRef.current = false) + }, []) + + return React.useCallback( + (...args) => (mountedRef.current ? dispatch(...args) : void 0), + [dispatch], + ) +} + +function asyncReducer(state, action) { + switch (action.type) { + case 'pending': { + return {status: 'pending', data: null, error: null} + } + case 'resolved': { + return {status: 'resolved', data: action.data, error: null} + } + case 'rejected': { + return {status: 'rejected', data: null, error: action.error} + } + default: { + throw new Error(`Unhandled action type: ${action.type}`) + } + } +} + +function useAsync(initialState) { + const [state, unsafeDispatch] = React.useReducer(asyncReducer, { + status: 'idle', + data: null, + error: null, + ...initialState, + }) + const dispatch = useSafeDispatch(unsafeDispatch) + const {data, error, status} = state + + const run = React.useCallback( + promise => { + dispatch({type: 'pending'}) + promise.then( + data => dispatch({type: 'resolved', data}), + error => dispatch({type: 'rejected', error}), + ) + }, + [dispatch], + ) + + return {error, status, data, run} +} + +function PokemonInfo({pokemonName}) { + const { + data: pokemon, + status, + error, + run, + } = useAsync({status: pokemonName ? 'pending' : 'idle'}) + + React.useEffect(() => { + if (!pokemonName) return + run(fetchPokemon(pokemonName)) + }, [pokemonName, run]) + + switch (status) { + case 'idle': + return Submit a pokemon + case 'pending': + return + case 'rejected': + throw error + case 'resolved': + return + default: + throw new Error('This should be impossible') + } +} + +function App() { + const [pokemonName, setPokemonName] = React.useState('') + + function handleSubmit(newPokemonName) { + setPokemonName(newPokemonName) + } + + function handleReset() { + setPokemonName('') + } + + return ( +
+ +
+
+ + + +
+
+ ) +} + +function AppWithUnmountCheckbox() { + const [mountApp, setMountApp] = React.useState(true) + return ( +
+ +
+ {mountApp ? : null} +
+ ) +} + +export default AppWithUnmountCheckbox diff --git a/src/exercise/02.js b/src/exercise/02.js index 654f19dd..df70dbd2 100644 --- a/src/exercise/02.js +++ b/src/exercise/02.js @@ -10,20 +10,16 @@ import { PokemonErrorBoundary, } from '../pokemon' -// 🐨 this is going to be our generic asyncReducer -function pokemonInfoReducer(state, action) { +function asyncReducer(state, action) { switch (action.type) { case 'pending': { - // 🐨 replace "pokemon" with "data" - return {status: 'pending', pokemon: null, error: null} + return {status: 'pending', data: null, error: null} } case 'resolved': { - // 🐨 replace "pokemon" with "data" (in the action too!) - return {status: 'resolved', pokemon: action.pokemon, error: null} + return {status: 'resolved', data: action.data, error: null} } case 'rejected': { - // 🐨 replace "pokemon" with "data" - return {status: 'rejected', pokemon: null, error: action.error} + return {status: 'rejected', data: null, error: action.error} } default: { throw new Error(`Unhandled action type: ${action.type}`) @@ -31,56 +27,42 @@ function pokemonInfoReducer(state, action) { } } -function PokemonInfo({pokemonName}) { - // 🐨 move all the code between the lines into a new useAsync function. - // 💰 look below to see how the useAsync hook is supposed to be called - // 💰 If you want some help, here's the function signature (or delete this - // comment really quick if you don't want the spoiler)! - // function useAsync(asyncCallback, initialState, dependencies) {/* code in here */} - - // -------------------------- start -------------------------- - - const [state, dispatch] = React.useReducer(pokemonInfoReducer, { - status: pokemonName ? 'pending' : 'idle', - // 🐨 this will need to be "data" instead of "pokemon" - pokemon: null, +function useAsync(asyncCallback, initialState, dependencies) { + const [state, dispatch] = React.useReducer(asyncReducer, { + status: 'idle', + data: null, error: null, + ...initialState, }) React.useEffect(() => { - // 💰 this first early-exit bit is a little tricky, so let me give you a hint: - // const promise = asyncCallback() - // if (!promise) { - // return - // } - // then you can dispatch and handle the promise etc... - if (!pokemonName) { - return - } + const promise = asyncCallback() + if (!promise) return dispatch({type: 'pending'}) - fetchPokemon(pokemonName).then( - pokemon => { - dispatch({type: 'resolved', pokemon}) + promise.then( + data => { + dispatch({type: 'resolved', data}) }, error => { dispatch({type: 'rejected', error}) }, ) - // 🐨 you'll accept dependencies as an array and pass that here. - // 🐨 because of limitations with ESLint, you'll need to ignore - // the react-hooks/exhaustive-deps rule. We'll fix this in an extra credit. - }, [pokemonName]) - // --------------------------- end --------------------------- + // eslint-disable-next-line react-hooks/exhaustive-deps + }, dependencies) - // 🐨 here's how you'll use the new useAsync hook you're writing: - // const state = useAsync(() => { - // if (!pokemonName) { - // return - // } - // return fetchPokemon(pokemonName) - // }, {/* initial state */}, [pokemonName]) - // 🐨 this will change from "pokemon" to "data" - const {pokemon, status, error} = state + return state +} + +function PokemonInfo({pokemonName}) { + const state = useAsync( + () => { + if (!pokemonName) return + return fetchPokemon(pokemonName) + }, + {}, + [pokemonName], + ) + const {data: pokemon, status, error} = state switch (status) { case 'idle': diff --git a/src/exercise/02.md b/src/exercise/02.md index 18f5b184..ea1c5bcd 100644 --- a/src/exercise/02.md +++ b/src/exercise/02.md @@ -386,4 +386,4 @@ those two situations. ## 🦉 Feedback Fill out -[the feedback form](https://ws.kcd.im/?ws=Advanced%20React%20Hooks%20%F0%9F%94%A5&e=02%3A%20useCallback%3A%20custom%20hooks&em=). +[the feedback form](https://ws.kcd.im/?ws=Advanced%20React%20Hooks%20%F0%9F%94%A5&e=02%3A%20useCallback%3A%20custom%20hooks&em=elvinciqueira%40gmail.com). diff --git a/src/exercise/03.extra-2.js b/src/exercise/03.extra-2.js index 29d9cd56..6fb36bc9 100644 --- a/src/exercise/03.extra-2.js +++ b/src/exercise/03.extra-2.js @@ -15,14 +15,22 @@ import { } from '../pokemon' import {useAsync} from '../utils' -// 🐨 Create a PokemonCacheContext +const PokemonCacheContext = React.createContext() -// 🐨 create a PokemonCacheProvider function -// 🐨 useReducer with pokemonCacheReducer in your PokemonCacheProvider -// 💰 you can grab the one that's in PokemonInfo -// 🐨 return your context provider with the value assigned to what you get back from useReducer -// 💰 value={[cache, dispatch]} -// 💰 make sure you forward the props.children! +function PokemonCacheProvider(props) { + const [cache, dispatch] = React.useReducer(pokemonCacheReducer, {}) + return +} + +function usePokemonCache() { + const context = React.useContext(PokemonCacheContext) + if (!context) { + throw new Error( + 'usePokemonCache must be used within a PokemonCacheProvider', + ) + } + return context +} function pokemonCacheReducer(state, action) { switch (action.type) { @@ -35,12 +43,19 @@ function pokemonCacheReducer(state, action) { } } -function PokemonInfo({pokemonName}) { - // 💣 remove the useReducer here (or move it up to your PokemonCacheProvider) - const [cache, dispatch] = React.useReducer(pokemonCacheReducer, {}) - // 🐨 get the cache and dispatch from useContext with PokemonCacheContext +function PokemonInfo({pokemonName: externalPokemonName}) { + const [cache, dispatch] = usePokemonCache() - const {data: pokemon, status, error, run, setData} = useAsync() + const pokemonName = externalPokemonName?.toLowerCase() + const { + data: pokemon, + status, + error, + run, + setData, + } = useAsync({ + status: pokemonName ? 'pending' : 'idle', + }) React.useEffect(() => { if (!pokemonName) { @@ -55,7 +70,7 @@ function PokemonInfo({pokemonName}) { }), ) } - }, [cache, pokemonName, run, setData]) + }, [cache, dispatch, pokemonName, run, setData]) if (status === 'idle') { return 'Submit a pokemon' @@ -69,8 +84,7 @@ function PokemonInfo({pokemonName}) { } function PreviousPokemon({onSelect}) { - // 🐨 get the cache from useContext with PokemonCacheContext - const cache = {} + const [cache] = usePokemonCache() return (
Previous Pokemon @@ -91,19 +105,19 @@ function PreviousPokemon({onSelect}) { } function PokemonSection({onSelect, pokemonName}) { - // 🐨 wrap this in the PokemonCacheProvider so the PreviousPokemon - // and PokemonInfo components have access to that context. return (
- -
- onSelect('')} - resetKeys={[pokemonName]} - > - - -
+ + +
+ onSelect('')} + resetKeys={[pokemonName]} + > + + +
+
) } diff --git a/src/exercise/03.js b/src/exercise/03.js index 4243e6b7..1c5df295 100644 --- a/src/exercise/03.js +++ b/src/exercise/03.js @@ -3,23 +3,29 @@ import * as React from 'react' -// 🐨 create your CountContext here with React.createContext +const CountContext = React.createContext() -// 🐨 create a CountProvider component here that does this: -// 🐨 get the count state and setCount updater with React.useState -// 🐨 create a `value` array with count and setCount -// 🐨 return your context provider with the value assigned to that array and forward all the other props -// 💰 more specifically, we need the children prop forwarded to the context provider +function CountProvider(props) { + const [count, setCount] = React.useState(0) + const value = [count, setCount] + return +} + +function useCount() { + const context = React.useContext(CountContext) + if (!context) { + throw new Error('useCount must be used within a CountProvider') + } + return context +} function CountDisplay() { - // 🐨 get the count from useContext with the CountContext - const count = 0 + const [count] = useCount() return
{`The current count is ${count}`}
} function Counter() { - // 🐨 get the setCount from useContext with the CountContext - const setCount = () => {} + const [, setCount] = useCount() const increment = () => setCount(c => c + 1) return } @@ -27,12 +33,10 @@ function Counter() { function App() { return (
- {/* - 🐨 wrap these two components in the CountProvider so they can access - the CountContext value - */} - - + + + +
) } diff --git a/src/exercise/03.md b/src/exercise/03.md index 05900185..56b7e54d 100644 --- a/src/exercise/03.md +++ b/src/exercise/03.md @@ -154,4 +154,4 @@ most has improved performance and maintainability characteristics. ## 🦉 Feedback Fill out -[the feedback form](https://ws.kcd.im/?ws=Advanced%20React%20Hooks%20%F0%9F%94%A5&e=03%3A%20useContext%3A%20simple%20Counter&em=). +[the feedback form](https://ws.kcd.im/?ws=Advanced%20React%20Hooks%20%F0%9F%94%A5&e=03%3A%20useContext%3A%20simple%20Counter&em=elvinciqueira%40gmail.com). diff --git a/src/exercise/04.js b/src/exercise/04.js index 87e74795..7b0dfa19 100644 --- a/src/exercise/04.js +++ b/src/exercise/04.js @@ -5,8 +5,7 @@ import * as React from 'react' function MessagesDisplay({messages}) { const containerRef = React.useRef() - // 🐨 replace useEffect with useLayoutEffect - React.useEffect(() => { + React.useLayoutEffect(() => { containerRef.current.scrollTop = containerRef.current.scrollHeight }) @@ -31,9 +30,9 @@ function sleep(time = 0) { function SlooooowSibling() { // try this with useLayoutEffect as well to see // how it impacts interactivity of the page before updates. - React.useEffect(() => { + React.useLayoutEffect(() => { // increase this number to see a more stark difference - sleep(300) + sleep(1000) }) return null } diff --git a/src/exercise/04.md b/src/exercise/04.md index c9f22fca..ad381f4f 100644 --- a/src/exercise/04.md +++ b/src/exercise/04.md @@ -47,4 +47,4 @@ making observable changes to the DOM, then it should happen in ## 🦉 Feedback Fill out -[the feedback form](https://ws.kcd.im/?ws=Advanced%20React%20Hooks%20%F0%9F%94%A5&e=04%3A%20useLayoutEffect%3A%20auto-scrolling%20textarea&em=). +[the feedback form](https://ws.kcd.im/?ws=Advanced%20React%20Hooks%20%F0%9F%94%A5&e=04%3A%20useLayoutEffect%3A%20auto-scrolling%20textarea&em=elvinciqueira%40gmail.com). diff --git a/src/exercise/05.js b/src/exercise/05.js index 05f7da2d..100791e4 100644 --- a/src/exercise/05.js +++ b/src/exercise/05.js @@ -4,23 +4,27 @@ import * as React from 'react' // 🐨 wrap this in a React.forwardRef and accept `ref` as the second argument -function MessagesDisplay({messages}) { +const MessagesDisplay = React.forwardRef(function MessagesDisplay( + {messages}, + ref, +) { const containerRef = React.useRef() React.useLayoutEffect(() => { scrollToBottom() }) - // 💰 you're gonna want this as part of your imperative methods - // function scrollToTop() { - // containerRef.current.scrollTop = 0 - // } + React.useImperativeHandle(ref, () => ({ + scrollToTop, + scrollToBottom, + })) + + function scrollToTop() { + containerRef.current.scrollTop = 0 + } function scrollToBottom() { containerRef.current.scrollTop = containerRef.current.scrollHeight } - // 🐨 call useImperativeHandle here with your ref and a callback function - // that returns an object with scrollToTop and scrollToBottom - return (
{messages.map((message, index, array) => ( @@ -31,7 +35,7 @@ function MessagesDisplay({messages}) { ))}
) -} +}) function App() { const messageDisplayRef = React.useRef() diff --git a/src/exercise/05.md b/src/exercise/05.md index 5a2be7db..50203be9 100644 --- a/src/exercise/05.md +++ b/src/exercise/05.md @@ -103,4 +103,4 @@ parent component can call those directly. ## 🦉 Feedback Fill out -[the feedback form](https://ws.kcd.im/?ws=Advanced%20React%20Hooks%20%F0%9F%94%A5&e=05%3A%20useImperativeHandle%3A%20scroll%20to%20top%2Fbottom&em=). +[the feedback form](https://ws.kcd.im/?ws=Advanced%20React%20Hooks%20%F0%9F%94%A5&e=05%3A%20useImperativeHandle%3A%20scroll%20to%20top%2Fbottom&em=elvinciqueira%40gmail.com). diff --git a/src/exercise/06.js b/src/exercise/06.js index eda2bf80..9082c2f8 100644 --- a/src/exercise/06.js +++ b/src/exercise/06.js @@ -3,10 +3,11 @@ import * as React from 'react' +const formatDebugValue = ({query, state}) => `\`${query}\` => ${state}` + function useMedia(query, initialState = false) { const [state, setState] = React.useState(initialState) - // 🐨 call React.useDebugValue here. - // 💰 here's the formatted label I use: `\`${query}\` => ${state}` + React.useDebugValue({query, state}, formatDebugValue) React.useEffect(() => { let mounted = true diff --git a/src/exercise/06.md b/src/exercise/06.md index da592feb..96140828 100644 --- a/src/exercise/06.md +++ b/src/exercise/06.md @@ -82,4 +82,4 @@ is not necessary, however, go ahead and give it a try anyway. ## 🦉 Feedback Fill out -[the feedback form](https://ws.kcd.im/?ws=Advanced%20React%20Hooks%20%F0%9F%94%A5&e=06%3A%20useDebugValue%3A%20useMedia&em=). +[the feedback form](https://ws.kcd.im/?ws=Advanced%20React%20Hooks%20%F0%9F%94%A5&e=06%3A%20useDebugValue%3A%20useMedia&em=elvinciqueira%40gmail.com).