diff --git a/playground/README.mdx b/playground/README.mdx deleted file mode 100644 index e5b97994..00000000 --- a/playground/README.mdx +++ /dev/null @@ -1,97 +0,0 @@ -# Latest Ref - -In our exercise, we have a `useDebounce` function that isn't working the way we -want with hooks. We're going to need to "change the default" using the latest -ref pattern. - -`debounce` is a pattern that's often used in user-input fields. For example, if -you've got a signup form where the user can select their username, you probably -want to validate for the user that the username is not taken. You want to do it -when the user's done typing but without requiring them to do anything to trigger -the validation. With a debounced function, you could say when the user stops -typing for 400ms you can trigger the validation. If they start typing again -after only 350ms then you want to start over and wait again until the user -pauses for 400ms. - -In this exercise, the `debounce` function is already written. Even the -`useDebounce` hook is implemented for you. Your job is to implement the latest -ref pattern to fix its behavior. - -Our example here is a counter button that has a debounced increment function. We -want to make it so this works: - -- The step is `1` -- The user clicks the button -- The user updates the step value to `2` -- The user clicks the button again (before the debounce timer completes) -- The debounce timer completes for both clicks -- The count value should be `2` (instead of `1`) - -(Keep in mind, the tests are there to help you know you got it right). - -Before continuing here, please familiarize yourself with the exercise and how -it's implemented... Got it? Great, let's continue. - -Right now, you can play around with two different problems with the way our -exercise is implemented: - -```ts -// option 1: -// ... -const increment = () => setCount(c => c + step) -const debouncedIncrement = useDebounce(increment, 3000) -// ... -``` - -The problem here is `useDebounce` list `increment` in the dependency list for -`useMemo`. For this reason, any time there's a state update, we create a _new_ -debounced version of that function so the `timer` in that debounce function's -closure is different from the previous which means we don't cancel that timeout. -Ultimate this is the bug our users experience: - -- The user clicks the button -- The user updates the step value -- The user clicks the button again -- The first debounce timer completes -- The count value is incremented by the step value at the time the first click - happened -- The second debounce timer completes -- The count value is incremented by the step value at the time the second click - happened - -This is not what we want at all! And the reason it's a problem is because we're -not memoizing the callback that's going into our `useMemo` dependency list. - -So the alternative solution is we could change our `useDebounce` API to require -you pass a memoized callback: - -```ts -// option 2: -// ... -const increment = React.useCallback(() => setCount(c => c + step), [step]) -const debouncedIncrement = useDebounce(increment, 3000) -// ... -``` - -But again, this callback function will be updated when the `step` value changes -which means we'll get another instance of the `debouncedIncrement`. Dah! So the -user experience doesn't actually change with this adjustment _and_ we have a -less fun API. The latest ref pattern will give us a nice API and we'll avoid -this problem. - -I've made the debounce value last `3000ms` to make it easier for you to observe -and test the behavior, but you can feel free to adjust that as you like. The -tests can also help you make sure you've got things working well. - -
-

Files

- - -
diff --git a/playground/index.tsx b/playground/index.tsx deleted file mode 100644 index 218105b6..00000000 --- a/playground/index.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import * as React from 'react' -import * as ReactDOM from 'react-dom/client' - -function debounce) => void>( - fn: Callback, - delay: number, -) { - let timer: ReturnType | null = null - return (...args: Parameters) => { - if (timer) clearTimeout(timer) - timer = setTimeout(() => { - fn(...args) - }, delay) - } -} - -function useDebounce) => unknown>( - callback: Callback, - delay: number, -) { - // 🐨 create a latest ref (via useRef and useEffect) here - - // use the latest version of the callback here: - // 💰 you'll need to pass an annonymous function to debounce. Do *not* - // simply change this to `debounce(latestCallbackRef.current, delay)` - // as that won't work. Can you think of why? - return React.useMemo(() => debounce(callback, delay), [callback, delay]) -} - -function App() { - const [step, setStep] = React.useState(1) - const [count, setCount] = React.useState(0) - - // 🦉 feel free to swap these two implementations and see they don't make - // any difference to the user experience - // const increment = React.useCallback(() => setCount(c => c + step), [step]) - const increment = () => setCount(c => c + step) - const debouncedIncrement = useDebounce(increment, 3000) - return ( -
-
- -
- -
- ) -} - -const rootEl = document.createElement('div') -document.body.append(rootEl) -ReactDOM.createRoot(rootEl).render()