React wrapper component: Scrollable
Tags: react, javascript, html, styledcomponents
Published: 2022-07-02 19:12:00

**The goal:**
We will create a React component which makes any given component vertically scrollable using the mouse wheel. In this example, we will make a data table scrollable: the table content will change when we roll the mouse wheel.

The Scrollable component's area is yellow, and the table inside becomes scrollable:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hz6wff3zog2q9jnccr72.png)

We create the following 2 files:

First, a new **React custom hook** (useScroll.ts) provides functionality to add a "wheel" event listener to the "ref" of the scroll box. It also automatically removes the event listener, so we will not have a memory leak. The event will call the provided "onScroll" function with "up" or "down" as parameter.
```js
import { useEffect } from "react";

const useScroll = (
  ref: React.RefObject<HTMLElement>,
  onScroll: (direction: "up" | "down") => void
) => {
  const callOnScroll = (event: Event): void => {
    // @ts-ignore
    const wheelEvent = event as React.WheelEvent<HTMLElement>;
    onScroll(wheelEvent.deltaY > 0 ? "up" : "down");
  };

  // ------------------------------------
  useEffect(() => {
    if (ref && ref.current) {
      ref?.current?.addEventListener("wheel", callOnScroll);
    }

    return () => {
      ref?.current?.removeEventListener("wheel", callOnScroll);
    };
  }, [ref, ref?.current, onScroll]);
};

export default useScroll;
```

(Note: What is a "React custom hook"? One definition, or my definition, is: if a function is named "use..", and calls at least one other React hook, then it's a "React custom hook".)

Then, inside our new **wrapper React component** (Scrollable.tsx), a "div" wraps other components as children. We create a "ref" for it so a "wheel" event can be attached to it, to call the provided "onScroll" function. We also change the cursor inside the div to show we have entered the scrollable area.
```ts
import React, { useRef } from "react";
import styled from "styled-components";

// @ts-ignore
import { useScroll } from "hooks";

const Container = styled.div`
  position: relative;
  background-color: beige;
  padding: 5px;
`;

const ScrollIcon = styled.div`
  position: absolute;
  right: 10px;
  font-size: 24px;
`;

type Props = {
  onScroll: (direction: "up" | "down") => void;
  children: React.ReactNode;
};

// ------------------------------------
const Scrollable = ({ onScroll, children }: Props) => {
  const refBoxWithScroll = useRef(null);

  useScroll(refBoxWithScroll, onScroll);

  // ------------------------------------
  return (
    <Container ref={refBoxWithScroll}>
      <ScrollIcon>🡙</ScrollIcon>
      {children}
    </Container>
  );
};

export default Scrollable;
```
_(If you wish, you can remove definitions for: styled & Container & ScrollIcon, and use a "div" instead of a "Container".)_

Finally, in another React component we wrap a table component inside the Scrollable. When the cursor is inside the Scrollable, moving the mouse wheel will call the given onScroll function. The onScroll function will increment or decrement an "offset" state variable. And the table will display 10 rows starting from this "offset", so the table will be scrolling.
```ts
...
const onSelectedFieldsTableScroll = (direction: "up" | "down") => {
  if (direction === "up" && matchOffset < matchIndexes.length - pageSize) {
    setMatchOffset(matchOffset + 1);
  } else if (direction === "down" && matchOffset > 0) {
    setMatchOffset(matchOffset - 1);
  }
};
...
return (
  ...
      <Scrollable onScroll={onSelectedFieldsTableScroll}>
        <SelectedFieldsTable rows={rows} />
      </Scrollable>
  ...
```

Corrections/suggestions are welcome.