@ -1899,4 +1899,613 @@ Sometimes, you don't even need a Hook!
- Don't create custom Hooks like `useMount`. Keep their purpose specific.
- It's up to you how and where to choose the boundaries of your code.
</Recap>
</Recap>
<Challenges>
### Extract a `useCounter` Hook {/*extract-a-usecounter-hook*/}
This component uses a state variable and an Effect to display a number that increments every second. Extract this logic into a custom Hook called `useCounter`. Your goal is to make the `Counter` component implementation look exactly like this:
```js
export default function Counter() {
const count = useCounter();
return <h1>Seconds passed: {count}</h1>;
}
```
You'll need to write your custom Hook in `useCounter.js` and import it into the `Counter.js` file.
<Sandpack>
```js
import { useState, useEffect } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>Seconds passed: {count}</h1>;
}
```
```js useCounter.js
// Write your custom Hook in this file!
```
</Sandpack>
<Solution>
Your code should look like this:
<Sandpack>
```js
import { useCounter } from './useCounter.js';
export default function Counter() {
const count = useCounter();
return <h1>Seconds passed: {count}</h1>;
}
```
```js useCounter.js
import { useState, useEffect } from 'react';
export function useCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return count;
}
```
</Sandpack>
Notice that `App.js` doesn't need to import `useState` or `useEffect` anymore.
</Solution>
### Make the counter delay configurable {/*make-the-counter-delay-configurable*/}
In this example, there is a `delay` state variable controlled by a slider, but its value is not used. Pass the `delay` value to your custom `useCounter` Hook, and change the `useCounter` Hook to use the passed `delay` instead of hardcoding `1000` ms.
<Sandpack>
```js
import { useState } from 'react';
import { useCounter } from './useCounter.js';
export default function Counter() {
const [delay, setDelay] = useState(1000);
const count = useCounter();
return (
<>
<label>
Tick duration: {delay} ms
<br/>
<input
type="range"
value={delay}
min="10"
max="2000"
onChange={e => setDelay(Number(e.target.value))}
/>
</label>
<hr/>
<h1>Ticks: {count}</h1>
</>
);
}
```
```js useCounter.js
import { useState, useEffect } from 'react';
export function useCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return count;
}
```
</Sandpack>
<Solution>
Pass the `delay` to your Hook with `useCounter(delay)`. Then, inside the Hook, use `delay` instead of the hardcoded `1000` value. You'll need to add `delay` to your Effect's dependencies. This ensures that a change in `delay` will reset the interval.
<Sandpack>
```js
import { useState } from 'react';
import { useCounter } from './useCounter.js';
export default function Counter() {
const [delay, setDelay] = useState(1000);
const count = useCounter(delay);
return (
<>
<label>
Tick duration: {delay} ms
<br/>
<input
type="range"
value={delay}
min="10"
max="2000"
onChange={e => setDelay(Number(e.target.value))}
/>
</label>
<hr/>
<h1>Ticks: {count}</h1>
</>
);
}
```
```js useCounter.js
import { useState, useEffect } from 'react';
export function useCounter(delay) {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, delay);
return () => clearInterval(id);
}, [delay]);
return count;
}
```
</Sandpack>
</Solution>
### Extract `useInterval` out of `useCounter` {/*extract-useinterval-out-of-usecounter*/}
Currently, your `useCounter` Hook does two things. It sets up an interval, and it also increments a state variable on every interval tick. Split out the logic that sets up the interval into a separate Hook called `useInterval`. It should take two arguments: the `onTick` callback, and the `delay`. After this change, your `useCounter` implementation should look like this:
```js
export function useCounter(delay) {
const [count, setCount] = useState(0);
useInterval(() => {
setCount(c => c + 1);
}, delay);
return count;
}
```
Write `useInterval` in the `useInterval.js` file and import it into the `useCounter.js` file.
<Sandpack>
```js
import { useState } from 'react';
import { useCounter } from './useCounter.js';
export default function Counter() {
const count = useCounter(1000);
return <h1>Seconds passed: {count}</h1>;
}
```
```js useCounter.js
import { useState, useEffect } from 'react';
export function useCounter(delay) {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, delay);
return () => clearInterval(id);
}, [delay]);
return count;
}
```
```js useInterval.js
// Write your Hook here!
```
</Sandpack>
<Solution>
The logic inside `useInterval` should set up and clear the interval. It doesn't need to do anything else.
<Sandpack>
```js
import { useCounter } from './useCounter.js';
export default function Counter() {
const count = useCounter(1000);
return <h1>Seconds passed: {count}</h1>;
}
```
```js useCounter.js
import { useState } from 'react';
import { useInterval } from './useInterval.js';
export function useCounter(delay) {
const [count, setCount] = useState(0);
useInterval(() => {
setCount(c => c + 1);
}, delay);
return count;
}
```
```js useInterval.js active
import { useEffect } from 'react';
export function useInterval(onTick, delay) {
useEffect(() => {
const id = setInterval(onTick, delay);
return () => clearInterval(id);
}, [onTick, delay]);
}
```
</Sandpack>
Note that there is a bit of a problem with this solution, which you'll solve in the next challenge.
</Solution>
### Fix a resetting interval {/*fix-a-resetting-interval*/}
In this example, there are *two* separate intervals.
The `App` component calls `useCounter`, which calls `useInterval` to update the counter every second. But the `App` component *also* calls `useInterval` to randomly update the page background color every two seconds.
For some reason, the callback that updates the page background never runs. Add some logs inside `useInterval`:
```js {2,5}
useEffect(() => {
console.log('✅ Setting up an interval with delay ', delay)
const id = setInterval(onTick, delay);
return () => {
console.log('❌ Clearing an interval with delay ', delay)
clearInterval(id);
};
}, [onTick, delay]);
```
Do the logs match what you expect to happen? If some of your Effects seem to re-synchronize unnecessarily, can you guess which dependency is causing that to happen? Is there some way to [remove that dependency](/learn/removing-effect-dependencies) from your Effect?
After you fix the issue, you should expect the page background to update every two seconds.
<Hint>
It looks like your `useInterval` Hook accepts an event listener as an argument. Can you think of some way to wrap that event listener so that it doesn't need to be a dependency of your Effect?
import { useRef, useInsertionEffect, useCallback } from 'react';
// The useEvent API has not yet been added to React,
// so this is a temporary shim to make this sandbox work.
// You're not expected to write code like this yourself.
export function useEvent(fn) {
const ref = useRef(null);
useInsertionEffect(() => {
ref.current = fn;
}, [fn]);
return useCallback((...args) => {
const f = ref.current;
return f(...args);
}, []);
}
```
</Sandpack>
<Solution>
Inside `useInterval`, wrap the tick callback into an Event function, as you did [earlier on this page.](/learn/reusing-logic-with-custom-hooks#passing-event-handlers-to-custom-hooks)
This will allow you to omit `onTick` from dependencies of your Effect. The Effect won't re-synchronize on every re-render of the component, so the page background color change interval won't get reset every second before it has a chance to fire.
With this change, both intervals work as expected and don't interfere with each other:
}, [delay, onTick]); // TODO: Linter will allow [delay] in the future
}
```
```js useEvent.js
import { useRef, useInsertionEffect, useCallback } from 'react';
// The useEvent API has not yet been added to React,
// so this is a temporary shim to make this sandbox work.
// You're not expected to write code like this yourself.
export function useEvent(fn) {
const ref = useRef(null);
useInsertionEffect(() => {
ref.current = fn;
}, [fn]);
return useCallback((...args) => {
const f = ref.current;
return f(...args);
}, []);
}
```
</Sandpack>
</Solution>
### Implement a staggering movement {/*implement-a-staggering-movement*/}
In this example, the `usePointerPosition()` Hook tracks the current pointer position. Try moving your cursor or your finger over the preview area and see the red dot follow your movement. Its position is saved in the `pos1` variable.
In fact, there are five (!) different red dots being rendered. You don't see them because currently they all appear at the same position. This is what you need to fix. What you want to implement instead is a "staggered" movement: each next dot should "follow" the previous dot's path. For example, if you quickly move your cursor, the first dot should follow it immediately, the second dot should follow the first dot with a small delay, the third dot should follow the second dot, and so on.
You need to implement the `useDelayedValue` custom Hook. Its current implementation returns the `value` provided to it. Instead, you want to return the value back from `delay` milliseconds ago. You might need some state and an Effect to do this.
After you implement `useDelayedValue`, you should see the dots move following one another.
<Hint>
You'll need to store the `delayedValue` as a state variable inside your custom Hook. When the `value` changes, you'll want to run an Effect. This Effect should update `delayedValue` after the `delay`. You might find it helpful to call `setTimeout`.
Does this Effect need cleanup? Why or why not?
</Hint>
<Sandpack>
```js
import { usePointerPosition } from './usePointerPosition.js';
Here is a working version. You keep the `delayedValue` as a state variable. When `value` updates, your Effect schedules a timeout to update the `delayedValue`. This is why the `delayedValue` always "lags behind" the actual `value`.
<Sandpack>
```js
import { useState, useEffect } from 'react';
import { usePointerPosition } from './usePointerPosition.js';
Note that this Effect *does not* need cleanup. If you called `clearTimeout` in the cleanup function, then each time the `value` changes, it would reset the already scheduled timeout. To keep the movement continuous, you want all the timeouts to fire.