Detecting the user movement and showing components based on direction and offset with useScroll hook.
For what useScroll hook can be used?
The useScroll hook can be used for reading scroll metadata connected with the target (HTML element or window object). It may be the direction in which the user is moving too or the exact scroll offset from the top/left.
The use cases
- Detecting the user movement and showing components based on direction and offset.
- Reducing calculations/computations for particular elements.
- Implementing mechanisms like infinite scrolling.
Here you have an example use case in which we're detecting bottom position of scroll and if it's achieved, we're rendering the menu.
As always under the following repository you have a full example implemented and ready to use.
The example of useScroll usage
The hook can be consumed in two ways:
- It can detect the window scroll metadata or any HTML element (for HTML elements you need to consume ref returned from hook and pass the type of an element).
- It can detect scroll metadata based on provided axis - it can be "x" or "y".
In addition, you can pass the delay property in ms. Inside the debounce mechanism is used to avoid too many checks during user interaction - it's just for sake of performance.
Types definition for useScroll hook
Types are very important. For the sake of transparency and readability, we moved them to separate file - in our case defs.ts. Let's start from the basic ones:
Now we need to define the shape of internal state. Let's think about the possible situations. During scrolling, user may have following scenarions:
- Nothing happens - let's call it "idle".
- User scrolled down or right - let's call it "progress".
- User scrolled up or left - let's call it "regress".
- User scrolled up/down but the scroll previous and current offset is the same - let's call it "unchanged".
So, we have 4 possible states. Now it's time to define interfaces for them. Every state will have a separate interface with shared property - let's call it is. We'll use a TypeScript union mechanism and we'll use the exhaustiveness checking technique. It will protect us from reading not "ready" information in runtime.
Now look at what will happen when you would like to read the state of a hook. TypeScript will block you from reading the prev/curr properties at the idle state but will let you read them in other states.
Now it's time for the last type. Let's define what the useScroll hook need to return. It will be a read-only tuple. The first element will be a state and the second will be the React ref object.
Now all types needs to be exported and we can start the hook implementation. Before that, it's good to mention some stuff:
- We used "T" generic to be able to pass the type of HTML element - for example HTMLDivElement, HTMLInputElement and others - it provides additional type-safety.
- The MutableRefObject takes a "T" or "null". It's because if we want to check the metadata of "window" instead of "HTMLElement", the ref is not needed so it will be a "null".
- We've used a "tuple" to provide developers an easy option to assign their own names for returned values.
Under this file, you have complete types definition.
Let's implement the useScroll hook
We have an interfaces so it's time to play! Let's consider some cases:
- The debounce mechanism must be applied. Listening to the scroll events and calling the callbacks too often, may hurt the performance.
- The hook may be used on the server side. So we need to guarantee that it will work there and not will not throw an exception during this phase.
- The properties responsible for reading scroll metadata for window and HTML element object differs. So we need to provide some unification for a better development experience.
- If the window object and HTML element both are undefined - it means the developer used the hook in an invalid way. So in this case we need to log a warning.
- The default parameters for the configuration object must be defined. The commonly used scroll is in the "y" axis and the "delay", can be 150ms by default.
Firstly, let's import previously defined types.
Now it's time for helper functions to read the scroll metadata according to the used window or HTML element objects.
And what about logging a warning for developers if the hook will be used in an invalid way?
Why these functions should be inside the same file as the future implemented useScroll hook? It's because of the internal implementation details. We want to hide everything that is related to these details from the consumer module.
Almost lastly, we need to create a hook itself.
Now when our hook matches the given contract defined by interfaces, we can implement the logic itself. We should use useLayoutEffect hook. It's because we're reading real DOM information so we need to have a 100% guarantee that they are ready to be read.
It's because useEffect is async and useLayoutEffect is sync. So, with useEffect our read data can be "old" and it may cause weird bugs...
We've used here rxjs library. Let's explain the stuff that we've used here.
- The "rxjs" library is tree-shakeable, so it means only imported, small functions will be bundled.
- This library provides reactivity and a lot of great for performance operators.
- It removes from us the need to maintain hard-to-implement code.
- It makes all async stuff much easier.
- It implements an observable pattern, in which we can subscribe to dedicated situations and react in our code.
There is one more small stuff to do. Our hook will work now on the client side perfectly, but on the server side (Next.js) we'll see a warning about the usage of useLayoutEffect hook on server. How to fix that?
We need to create a small util that is often called by developers as a useIsomorphicLayoutEffect.
You can read more about this in the following 🔰 Removing server warnings for useLayoutEffect with custom hook article.
Lastly, we need to replace "useLayoutEffect" with a newly created hook.
We got that! Here is the final result of how it looks like.
The tests for useScroll hook
We wanted to explain the hook usage and implementation only. Tests are under the following path. If you are interested how they're implemented just visit this url.
In addition, there are following articles that may be helpfull.
The aticle about fixtures pattern - Creating testing fixtures.
The aticle about snapshot testing - Snapshot testing in React.
The full course about testing in React ecosystem - React testing spellbook.
The final example in repository
As always under the following repository you have a full example with tests.
Summary of the useScroll hook
The hook created by us, contains great type-safety and it's safe to use on the client/server side. In addition, it provides developer hints when it will be used in an invalid way and protects us from performance problems due to scroll events.
Nextly, it allows you to listen on both x and y axis scroll events. It's easy to extend - if you need other metadata you can just increase the interface properties.
And what is the most important, we have tests - if you are curious just check them in the final example.
- 1. Rendering
Creating portals with custom usePortal hook
We will change the screens with useStepper hook
Manage components appearance with useToggle hook
Removing server warnings for useLayoutEffect with custom hook
First interaction detection with useOnInteraction hook
- 2. Forms
- 3. Events
Read the scroll metadata and direction with useScroll hook
Using clipboard with useClipboard hook
Detect outside click with the useClickOutside hook
Deep dive into useIntersectionObserver hook
Element size measurement with useElementSize hook
- 4. Guards
- 5. Interactions