Lightweight React AoS
Tags: react, webdev, frontend, typescript
Published: 2023-05-06 10:23:41

In today's modern websites, having animation in your website is a must, and some simple animations can add a lot to your website, that's why AoS library is so popular, but what about creating an alternative for it on your own?

## Our starting point
I didn't write the code for this component from scratch, the code I started from was by [Bret Cameron](https://betterprogramming.pub/simple-react-scroll-animations-with-zero-dependencies-b496c1e1c7bd), and from the next section in this blog, I'll assume that you went through his code and got an idea about what this component does.

*Note*
When I copied Bret's code from [his codepen](https://codepen.io/BretCameron/pen/RwyRVmy), I got an issue because of how he imported the types, I got this error `'X' refers to a value, but is being used as a type here. Did you mean 'typeof X'?`, with X being any type I'm importing like `CSSProperties` or `RefObject`, I solved this error by using `import { ... } from "react"` instead of `const {...} = React`.

### Issues with this code

#### overriding default values

there is no way to change the values for the `defaultStyles` in the `AnimateIn` function, which is quite problematic because you might want to add a delay for some sections, change the easing option...etc. To solve this issue let's first create an interface for our animation components.

```ts
interface IAnimation {
  children: ReactNode;
  transitionProps?: CSSProperties;
}
```

Now let's use this interface in our animation components, and let's just try the FadeIn.

```ts
const FadeIn = (props: IAnimation) => (
  <Animate
    from={{ opacity: 0 }}
    to={{ opacity: 1 }}
    transitionProps={props.transitionProps}
  >
    {props.children}
  </Animate>
);
```
*Note: I changed the name of* `AnimateIn` *function to* `Animate`.

Since we have now passed our transitionProps we should handle them in our `Animate` function, creating an interface for that is not necessary but I just like to use interfaces.

```ts
interface IAnimate extends IAnimation {
  from: CSSProperties;
  to: CSSProperties;
}
```

Let's add a condition to handle when to use the default props and when to use the passed props like this:

```ts
const styleProps = props.transitionProps
    ? props.transitionProps
    : defaultStyles;
```

And we just use `styleProps` as we did with the `defaultStyles`.

### A "run once" option

Now, Everything runs perfectly, but there is one issue, that I'd consider a huge one, which is when you scroll down and one of the sections that you are observing is now on the screen and its animation has started then you scroll down and go back up the animation will start again, in most websites you don’t see that, because it’s kinda annoying to have animations that start every time you scroll to a section, so for that reason, we should add a "run-once" option, but I'm not gonna make it the only option, because there might be some use cases where the animation have to start every time the user scrolls to the section. First of all, we update our `IAnimation` interface to have a runOnce prop.

```ts
interface IAnimation {
  children: ReactNode;
  runOnce?: boolean;
  transitionProps?: CSSProperties;
}
```

Also, add a parameter to the `useElementOnScreen` hook to handle the run once mode.

```ts
const useElementOnScreen = (
  ref: RefObject<Element>,
  runOnce: boolean | undefined
) => { ... }
```

And the logic for it is fairly simple, everything runs as normal, we keep observing the section that we want, and we check if it has been on the screen, if it does ( the animation has started ), then we check if `runOnce` is true, if it is we unobserve this section, otherwise, we just keep observing and triggering the animation again and again when the element enters the screen. So let's take the process for this one step by step.

1. First we specify the `runOnce` argument in one of our animation divs like this.

    ```tsx
    <AoS.FadeUp runOnce>
     <AnySection/>
    </AoS.FadeUp>
    ```
    *Note: the default for this option is true, so you don't have 
     to specify it unless you want to set it to false.*

2. since we have its value, we can now access it via the `useRef` hook in the `Animate`, then pass it to the `useElementOnScreen` hook like this.

    ```tsx
    const { current: runOnce } = useRef(props.runOnce);
    const ref = useRef<HTMLDivElement>(null);
    const onScreen = useElementOnScreen(ref, runOnce);
    ```
3. Inside the `useElementOnScreen` hook we will add an if statement for that option like this.

```ts
const useElementOnScreen = (
  ref: RefObject<Element>,
  runOnce: boolean | undefined
) => {
  const [isIntersecting, setIsIntersecting] = useState(true);
  useEffect(() => {
    const observer = new IntersectionObserver(
    ([entry]) => {
      if (runOnce && ref.current && entry.isIntersecting) {
        observer.unobserve(ref.current);
     }
  ...
```

In this if statement, first we check if the `runOnce` option is true, then we check if the element that we are passing exists, and last with `entry.isIntersecting` we check if the element has transitioned into the intersection state, which means that It has been on the screen and its animation has started.

*For more information about the intersection observer API visit the MDN [docs](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).*

## And that's it...

Now you can just copy this [file](https://github.com/hossammenem/hossamabdelmenem-Portfolio/blob/main/components/AoS.tsx), add any your changes and start using it in your next project.

If you wanna see some of those animations on a live website, I'm already using them in my [portfolio](https://www.notion.so/hossamabdelmenem-portfolio.vercel.app), so feel free to check it out.